diff --git a/package-lock.json b/package-lock.json index cbc711a9..e584186c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "yesno": "^0.4.0" }, "bin": { - "frodo": "esm/app.js" + "frodo": "esm/launch.js" }, "devDependencies": { "@babel/eslint-parser": "^7.18.9", diff --git a/package.json b/package.json index 26969168..9f173426 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "devops" ], "engines": { - "node": ">=14" + "node": ">=16" }, "repository": { "type": "git", @@ -27,7 +27,7 @@ "bugs": { "url": "https://github.com/rockcarver/frodo-cli/issues" }, - "main": "esm/app.js", + "main": "esm/launch.js", "scripts": { "test": "npx tsc && node --experimental-vm-modules node_modules/jest/bin/jest.js", "test:local": "npm run build && node --experimental-vm-modules node_modules/jest/bin/jest.js", @@ -78,7 +78,7 @@ ], "license": "MIT", "bin": { - "frodo": "./esm/app.js" + "frodo": "./esm/launch.js" }, "babel": { "plugins": [ diff --git a/src/app.ts b/src/app.ts index 3b592237..29d2e87f 100755 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env -S node --no-warnings --enable-source-maps --experimental-specifier-resolution=node - import { ConnectionProfile } from '@rockcarver/frodo-lib'; import { Command } from 'commander'; diff --git a/src/launch.ts b/src/launch.ts new file mode 100755 index 00000000..6f118861 --- /dev/null +++ b/src/launch.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); + +const launchArgs = [ + '--no-warnings', + '--enable-source-maps', + '--experimental-loader', + require.resolve('./loader.js'), + require.resolve('./app.js'), +]; +const frodoArgs = process.argv.slice(2); + +spawn(process.execPath, [...launchArgs, ...frodoArgs], { + stdio: 'inherit', + shell: false, +}); diff --git a/src/loader.ts b/src/loader.ts new file mode 100644 index 00000000..c19b5e88 --- /dev/null +++ b/src/loader.ts @@ -0,0 +1,44 @@ +import { builtinModules } from 'node:module'; +import { dirname } from 'path'; +import { cwd } from 'process'; +import { fileURLToPath, pathToFileURL } from 'url'; +import { promisify } from 'util'; + +import resolveCallback from 'resolve/async.js'; + +const resolveAsync = promisify(resolveCallback); + +const baseURL = pathToFileURL(cwd() + '/').href; + +export async function resolve(specifier, context, next) { + const { parentURL = baseURL } = context; + + if (specifier.startsWith('node:') || builtinModules.includes(specifier)) { + return next(specifier, context); + } + + // `resolveAsync` works with paths, not URLs + if (specifier.startsWith('file://')) { + specifier = fileURLToPath(specifier); + } + const parentPath = fileURLToPath(parentURL); + + let url; + try { + const resolution = await resolveAsync(specifier, { + basedir: dirname(parentPath), + // For whatever reason, --experimental-specifier-resolution=node doesn't search for .mjs extensions + // but it does search for index.mjs files within directories + extensions: ['.js', '.json', '.node', '.mjs'], + }); + url = pathToFileURL(resolution).href; + } catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + // Match Node's error code + error.code = 'ERR_MODULE_NOT_FOUND'; + } + throw error; + } + + return next(url, context); +}