diff --git a/bin/unbuild.mjs b/bin/unbuild.mjs deleted file mode 100755 index 318916d1..00000000 --- a/bin/unbuild.mjs +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import '../dist/cli.mjs' diff --git a/package.json b/package.json index 76d42234..20116469 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,9 @@ "exports": "./dist/index.mjs", "types": "dist/index.d.ts", "bin": { - "unbuild": "./bin/unbuild.mjs" + "unbuild": "./dist/cli.mjs" }, "files": [ - "bin", "dist" ], "scripts": { diff --git a/src/builder/plugins/shebang.ts b/src/builder/plugins/shebang.ts new file mode 100644 index 00000000..6fd424d6 --- /dev/null +++ b/src/builder/plugins/shebang.ts @@ -0,0 +1,60 @@ +import { promises as fsp } from 'fs' +import MagicString from 'magic-string' +import { resolve } from 'pathe' +import type { Plugin } from 'rollup' + +// Forked from https://github.com/developit/rollup-plugin-preserve-shebang (1.0.1 @ MIT) + +const SHEBANG_RE = /^#![^\n]*/ + +export interface ShebangPluginOptions { + preserve?: Boolean +} + +export function shebangPlugin (options: ShebangPluginOptions = {}): Plugin { + const shebangs = new Map() + return { + name: 'unbuild-shebang', + // @ts-ignore temp workaround + _options: options, + transform (code, mod) { + let shebang + code = code.replace(SHEBANG_RE, (match) => { + shebang = match + return '' + }) + if (!shebang) { return null } + shebangs.set(mod, shebang) + return { code, map: null } + }, + renderChunk (code, chunk, { sourcemap }) { + if (options.preserve === false) { return null } + const shebang = shebangs.get(chunk.facadeModuleId) + if (!shebang) { return null } + const s = new MagicString(code) + s.prepend(`${shebang}\n`) + return { + code: s.toString(), + map: sourcemap ? s.generateMap({ hires: true }) : null + } + }, + async writeBundle (options, bundle) { + for (const [fileName, output] of Object.entries(bundle)) { + if (output.type !== 'chunk') { continue } + if (output.code?.match(SHEBANG_RE)) { + const outFile = resolve(options.dir!, fileName) + await makeExecutable(outFile) + } + } + } + } +} + +export async function makeExecutable (filePath: string) { + await fsp.chmod(filePath, 0o755 /* rwx r-x r-x */).catch(() => {}) +} + +export function getShebang (code: string, append = '\n') { + const m = code.match(SHEBANG_RE) + return m ? m + append : '' +} diff --git a/src/builder/rollup.ts b/src/builder/rollup.ts index e5ce2271..172c1641 100644 --- a/src/builder/rollup.ts +++ b/src/builder/rollup.ts @@ -1,4 +1,5 @@ import { writeFile } from 'fs/promises' +import { promises as fsp } from 'fs' import type { RollupOptions, OutputOptions, OutputChunk } from 'rollup' import { rollup } from 'rollup' import commonjs from '@rollup/plugin-commonjs' @@ -8,11 +9,12 @@ import _esbuild from 'rollup-plugin-esbuild' import dts from 'rollup-plugin-dts' import { relative, resolve } from 'pathe' import consola from 'consola' -import { getpkg } from '../utils' +import { getpkg, tryResolve } from '../utils' import type { BuildContext } from '../types' import { JSONPlugin } from './plugins/json' import { rawPlugin } from './plugins/raw' import { cjsPlugin } from './plugins/cjs' +import { shebangPlugin, makeExecutable, getShebang } from './plugins/shebang' // @ts-ignore https://github.com/unjs/unbuild/issues/23 const esbuild = _esbuild.default || _esbuild @@ -21,11 +23,21 @@ export async function rollupBuild (ctx: BuildContext) { if (ctx.options.stub) { for (const entry of ctx.options.entries.filter(entry => entry.builder === 'rollup')) { const output = resolve(ctx.options.rootDir, ctx.options.outDir, entry.name!) + + const resolvedEntry = tryResolve(entry.input, ctx.options.rootDir) || entry.input + const code = await fsp.readFile(resolvedEntry, 'utf8') + const shebang = getShebang(code) + if (ctx.options.rollup.emitCJS) { - await writeFile(output + '.cjs', `module.exports = require('jiti')(null, { interopDefault: true })('${entry.input}')`) + await writeFile(output + '.cjs', `${shebang}module.exports = require('jiti')(null, { interopDefault: true })('${entry.input}')`) } - await writeFile(output + '.mjs', `import jiti from 'jiti';\nexport default jiti(null, { interopDefault: true })('${entry.input}');`) + await writeFile(output + '.mjs', `${shebang}import jiti from 'jiti';\nexport default jiti(null, { interopDefault: true })('${entry.input}');`) await writeFile(output + '.d.ts', `export * from '${entry.input}';\nexport { default } from '${entry.input}';`) + + if (shebang) { + await makeExecutable(output + '.cjs') + await makeExecutable(output + '.mjs') + } } await ctx.hooks.callHook('rollup:done', ctx) return @@ -61,6 +73,9 @@ export async function rollupBuild (ctx: BuildContext) { // Types if (ctx.options.declaration) { rollupOptions.plugins = rollupOptions.plugins || [] + // TODO: Use fresh rollup options + const shebangPlugin: any = rollupOptions.plugins.find(p => p && p.name === 'unbuild-shebang') + shebangPlugin._options.preserve = false rollupOptions.plugins.push(dts({ respectExternal: true })) await ctx.hooks.callHook('rollup:dts:options', ctx, rollupOptions) const typesBuild = await rollup(rollupOptions) @@ -143,6 +158,8 @@ export function getRollupOptions (ctx: BuildContext): RollupOptions { preferConst: true }), + shebangPlugin(), + esbuild({ target: 'es2020' }), @@ -158,6 +175,7 @@ export function getRollupOptions (ctx: BuildContext): RollupOptions { ctx.options.rollup.cjsBridge && cjsPlugin({}), rawPlugin() + ].filter(Boolean) } as RollupOptions } diff --git a/src/cli.ts b/src/cli.ts old mode 100644 new mode 100755 index 657f7c52..960fbc94 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,4 @@ +#!/bin/env node import { resolve } from 'pathe' import mri from 'mri' import { build } from './build' diff --git a/src/utils.ts b/src/utils.ts index 2429907d..b0fdd497 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -44,3 +44,15 @@ export function tryRequire (id: string, rootDir: string = process.cwd()) { return {} } } + +export function tryResolve (id: string, rootDir: string = process.cwd()) { + const _require = jiti(rootDir, { interopDefault: true }) + try { + return _require.resolve(id) + } catch (err: any) { + if (err.code !== 'MODULE_NOT_FOUND') { + console.error(`Error trying import ${id} from ${rootDir}`, err) + } + return id + } +}