UNPKG

@syncify/acquire

Version:

File require and import helper with TS Transform support

251 lines (185 loc) 6.43 kB
import type { AcquireOptions } from './types'; import type { BuildContext, Message } from 'esbuild'; import fs, { existsSync } from 'fs'; import { basename } from 'path'; import { build, BuildOptions, BuildResult, context } from 'esbuild'; import { REGEX_EXTJS, REGEX_NODE_MODULES } from './const'; import { AcquireError } from './error'; import { esbuildExternalPlugin, esbuildInjectionPlugin } from './plugins'; import { $tsconfig, tsconfigPaths } from './tsconfig'; import { $import, CJSorESM, outfile } from './utils'; export { $import, $require } from './utils'; export { $tsconfig }; export { AcquireError }; export function acquire<T = any> (options: AcquireOptions): Promise<T> { options.preserve = 'preserve' in options ? options.preserve : false; options.tsconfig = 'tsconfig' in options ? options.tsconfig : 'tsconfig.json'; return new Promise((resolve, reject) => { if (!REGEX_EXTJS.test(options.file)) { throw new AcquireError('Invalid javascript file', { file: options.file, details: 'The file extension must be: mjs, cjs, ts or js' }); } /* -------------------------------------------- */ /* PRESETS */ /* -------------------------------------------- */ const cwd = options.cwd || process.cwd(); const onError = typeof options.onError === 'function'; const onWarning = typeof options.onWarning === 'function'; const onRebuild = typeof options.onRebuild === 'function'; const format = CJSorESM(options.file, options.type); const name = typeof options.named === 'string' ? options.named : false; const tsconfig = options.tsconfig === false ? undefined : typeof options.tsconfig === 'string' ? $tsconfig(cwd, options.tsconfig as string) : { data: options.tsconfig, path: undefined }; const esbuild: BuildOptions = { entryPoints: [ options.file ], absWorkingDir: cwd, format, platform: 'node', sourcemap: false, bundle: true, logLevel: 'silent', splitting: false, metafile: true, write: false, ...(tsconfig?.path) ? { tsconfig: tsconfig.path } : { tsconfigRaw: tsconfig?.data || {} }, plugins: [ esbuildExternalPlugin({ external: options.external, externalNodeModules: options.externalNodeModules ?? !options.file.match(REGEX_NODE_MODULES), notExternal: [ ...(options.noExternal || []), ...tsconfigPaths(tsconfig?.data?.compilerOptions?.paths || {}) ] }), esbuildInjectionPlugin() ] }; /* -------------------------------------------- */ /* COMPILE */ /* -------------------------------------------- */ compile().catch(reject); /* -------------------------------------------- */ /* FUNCTIONS */ /* -------------------------------------------- */ /** * Compile * * Triggers esbuild build or content (depending on whether onRebuild) was called. */ async function compile () { if (onRebuild) { esbuild.plugins.push({ name: 'acquire:rebuild', setup (context) { let count = 0; context.onEnd(async result => { if (count++ === 0) { if (result.errors.length > 0) { errors(result.errors); } else { resolve(await bundle(result)); } } else { result.errors.length > 0 ? errors(result.errors) : options.onRebuild(await bundle(result)); } }); } }); const rebuild = await context(esbuild); await rebuild.watch(); acquire.isWatching = true; acquire.modules.set(name || basename(options.file), rebuild); } else { const result = await build(esbuild); const handle = await bundle<T>(result); resolve(handle); } }; /** * Remove generated bundle file */ async function unlink (path: string) { if (!options.preserve && existsSync(path)) { await fs.promises.unlink(path); // Remove the outfile after executed } } /** * Bundle Errors * * Triggers `options.error` callback (if defined) and return `null` on resolution */ function errors (result: Message[]) { return onError ? options.onError(result) : null; } /** * Bundle * * Processes the ESBuild result, writes temporary file, applies the correct * import resolution and reports on warnings or errors. * * Returns the module itself. */ async function bundle <T> (result: BuildResult): Promise<T> { const output = outfile(options.file, format); if (!result.outputFiles) { await unlink(output); throw new AcquireError('No output files', { file: options.file, details: 'ESBuild executed the build but failed to return output' }); } const { text } = result.outputFiles[0]; await fs.promises.writeFile(output, text, 'utf8'); let $module: any; try { $module = await $import(output, { format }); } catch (e) { await unlink(output); throw new AcquireError(e, { file: options.file, details: 'Import failed following acquire build' }); } finally { await unlink(output); } if (onWarning && result.warnings.length > 0) { options.onWarning(result.warnings); } const returns = name ? name in $module ? $module[name] : $module.default || $module : $module.default || $module; return returns; }; }); }; acquire.modules = new Map<string, BuildContext>(); acquire.isWatching = true; /** * Acquire Cancel * * Applies build cancellation in rebuild context */ acquire.cancel = function (name: string) { if (acquire.modules.has(name)) { return acquire.modules.get(name).cancel(); } }; /** * Acquire Disposal * * Applies disposal of a rebuild context. */ acquire.dispose = function (name: string) { if (acquire.modules.has(name)) { return acquire.modules.get(name).dispose(); } };