From b82b5ba4e57227de1382c1e3f1cc1263feaa7893 Mon Sep 17 00:00:00 2001 From: Weixin Date: Tue, 19 Mar 2024 14:08:52 -0400 Subject: [PATCH] feat: add support for preloading ES module with --import flag (#713) * feat: add support for preloading ES module with --import flag use --import or -i to preload ES module * docs: add docs for --import flag * fix: fix minor typos * docs: add the --import option to the start command usage file --------- Co-authored-by: Weixin Wu --- README.md | 1 + args.js | 4 ++- examples/plugin-with-preloaded.js | 5 +-- help/start.txt | 4 +++ start.js | 59 ++++++++++++++++++++++--------- test/args.test.js | 14 +++++++- test/data/custom-import.mjs | 3 ++ test/data/custom-import2.mjs | 3 ++ test/start.test.js | 51 +++++++++++++++++++++++++- util.js | 19 +++++++++- 10 files changed, 141 insertions(+), 22 deletions(-) create mode 100644 test/data/custom-import.mjs create mode 100644 test/data/custom-import2.mjs diff --git a/README.md b/README.md index 44cf970b..99921141 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ You can pass the following options via CLI arguments. You can also use `--config | Address to listen on | `-a` | `--address` | `FASTIFY_ADDRESS` | | Socket to listen on | `-s` | `--socket` | `FASTIFY_SOCKET` | | Module to preload | `-r` | `--require` | `FASTIFY_REQUIRE` | +| ES Module to preload | `-i` | `--import` | `FASTIFY_IMPORT` | | Log level (default to fatal) | `-l` | `--log-level` | `FASTIFY_LOG_LEVEL` | | Path to logging configuration module to use | `-L` | `--logging-module` | `FASTIFY_LOGGING_MODULE` | | Start Fastify app in debug mode with nodejs inspector | `-d` | `--debug` | `FASTIFY_DEBUG` | diff --git a/args.js b/args.js index 0e0307d6..1f842bc2 100644 --- a/args.js +++ b/args.js @@ -28,7 +28,7 @@ module.exports = function parseArgs (args) { 'populate--': true }, number: ['port', 'inspect-port', 'body-limit', 'plugin-timeout', 'close-grace-delay'], - string: ['log-level', 'address', 'socket', 'prefix', 'ignore-watch', 'logging-module', 'debug-host', 'lang', 'require', 'config', 'method'], + string: ['log-level', 'address', 'socket', 'prefix', 'ignore-watch', 'logging-module', 'debug-host', 'lang', 'require', 'import', 'config', 'method'], boolean: ['pretty-logs', 'options', 'watch', 'verbose-watch', 'debug', 'standardlint', 'common-prefix', 'include-hooks'], envPrefix: 'FASTIFY_', alias: { @@ -41,6 +41,7 @@ module.exports = function parseArgs (args) { watch: ['w'], prefix: ['x'], require: ['r'], + import: ['i'], debug: ['d'], 'debug-port': ['I'], 'log-level': ['l'], @@ -86,6 +87,7 @@ module.exports = function parseArgs (args) { address: parsedArgs.address, socket: parsedArgs.socket, require: parsedArgs.require, + import: parsedArgs.import, prefix: parsedArgs.prefix, loggingModule: parsedArgs.loggingModule, lang: parsedArgs.lang, diff --git a/examples/plugin-with-preloaded.js b/examples/plugin-with-preloaded.js index 9b8435dd..5dc93f5a 100644 --- a/examples/plugin-with-preloaded.js +++ b/examples/plugin-with-preloaded.js @@ -1,12 +1,13 @@ -/* global GLOBAL_MODULE_1 */ +/* global GLOBAL_MODULE_1, GLOBAL_MODULE_3 */ 'use strict' const t = require('tap') module.exports = async function (fastify, options) { fastify.get('/', async function (req, reply) { - return { hasPreloaded: GLOBAL_MODULE_1 } + return { hasPreloaded: GLOBAL_MODULE_1 && GLOBAL_MODULE_3 } }) fastify.addHook('onReady', function () { t.ok(GLOBAL_MODULE_1) + t.ok(GLOBAL_MODULE_3) }) } diff --git a/help/start.txt b/help/start.txt index 84b642da..95b526bf 100644 --- a/help/start.txt +++ b/help/start.txt @@ -22,6 +22,10 @@ OPTS [env: FASTIFY_REQUIRE] Module to preload + -i, --import + [env: FASTIFY_IMPORT] + ES Module to preload + -L, --logging-module [env: FASTIFY_LOGGING_MODULE] Path to logging configuration module to use diff --git a/start.js b/start.js index ddc1c113..4f650ef8 100755 --- a/start.js +++ b/start.js @@ -15,6 +15,7 @@ const parseArgs = require('./args') const { exit, requireModule, + requireESModule, requireFastifyForModule, requireServerPluginFromPath, showHelpForCommand, @@ -57,26 +58,52 @@ function stop (message) { exit(message) } +function preloadCJSModules (opts) { + if (typeof opts.require === 'string') { + opts.require = [opts.require] + } + try { + opts.require.forEach(module => { + if (module) { + /* This check ensures we ignore `-r ""`, trailing `-r`, or + * other silly things the user might (inadvertently) be doing. + */ + requireModule(module) + } + }) + } catch (e) { + module.exports.stop(e) + } +} + +async function preloadESModules (opts) { + if (typeof opts.import === 'string') { + opts.import = [opts.import] + } + opts.import.forEach(async (m) => { + if (m) { + /* This check ensures we ignore `-i ""`, trailing `-i`, or + * other silly things the user might (inadvertently) be doing. + */ + try { + await requireESModule(m) + } catch (e) { + module.exports.stop(e) + } + } + }) +} + async function runFastify (args, additionalOptions, serverOptions) { const opts = parseArgs(args) - if (opts.require) { - if (typeof opts.require === 'string') { - opts.require = [opts.require] - } - try { - opts.require.forEach(module => { - if (module) { - /* This check ensures we ignore `-r ""`, trailing `-r`, or - * other silly things the user might (inadvertently) be doing. - */ - requireModule(module) - } - }) - } catch (e) { - module.exports.stop(e) - } + if (opts.require) { + preloadCJSModules(opts) } + if (opts.import) { + await preloadESModules(opts) + } + opts.port = opts.port || process.env.PORT || 3000 loadModules(opts) diff --git a/test/args.test.js b/test/args.test.js index 0b3788c4..5d024a83 100644 --- a/test/args.test.js +++ b/test/args.test.js @@ -12,6 +12,7 @@ test('should parse args correctly', t => { '--address', 'fastify.dev:9999', '--socket', 'fastify.dev.socket:9999', '--require', './require-module.js', + '--import', './import-module.js', '--log-level', 'info', '--pretty-logs', 'true', '--watch', 'true', @@ -42,6 +43,7 @@ test('should parse args correctly', t => { address: 'fastify.dev:9999', socket: 'fastify.dev.socket:9999', require: './require-module.js', + import: './import-module.js', logLevel: 'info', prefix: 'FASTIFY_', pluginTimeout: 500, @@ -67,6 +69,7 @@ test('should parse args with = assignment correctly', t => { '--address=fastify.dev:9999', '--socket=fastify.dev.socket:9999', '--require', './require-module.js', + '--import', './import-module.js', '--log-level=info', '--pretty-logs=true', '--watch=true', @@ -97,6 +100,7 @@ test('should parse args with = assignment correctly', t => { address: 'fastify.dev:9999', socket: 'fastify.dev.socket:9999', require: './require-module.js', + import: './import-module.js', logLevel: 'info', prefix: 'FASTIFY_', pluginTimeout: 500, @@ -121,6 +125,7 @@ test('should parse env vars correctly', t => { process.env.FASTIFY_ADDRESS = 'fastify.dev:9999' process.env.FASTIFY_SOCKET = 'fastify.dev.socket:9999' process.env.FASTIFY_REQUIRE = './require-module.js' + process.env.FASTIFY_IMPORT = './import-module.js' process.env.FASTIFY_LOG_LEVEL = 'info' process.env.FASTIFY_PRETTY_LOGS = 'true' process.env.FASTIFY_WATCH = 'true' @@ -141,6 +146,7 @@ test('should parse env vars correctly', t => { delete process.env.FASTIFY_ADDRESS delete process.env.FASTIFY_SOCKET delete process.env.FASTIFY_REQUIRE + delete process.env.FASTIFY_IMPORT delete process.env.FASTIFY_LOG_LEVEL delete process.env.FASTIFY_PRETTY_LOGS delete process.env.FASTIFY_WATCH @@ -173,6 +179,7 @@ test('should parse env vars correctly', t => { prefix: 'FASTIFY_', socket: 'fastify.dev.socket:9999', require: './require-module.js', + import: './import-module.js', pluginTimeout: 500, closeGraceDelay: 30000, pluginOptions: {}, @@ -188,7 +195,7 @@ test('should parse env vars correctly', t => { }) test('should respect default values', t => { - t.plan(13) + t.plan(14) const argv = [ 'app.js' @@ -209,6 +216,7 @@ test('should respect default values', t => { t.equal(parsedArgs.debugPort, 9320) t.equal(parsedArgs.loggingModule, undefined) t.equal(parsedArgs.require, undefined) + t.equal(parsedArgs.import, undefined) }) test('should parse custom plugin options', t => { @@ -219,6 +227,7 @@ test('should parse custom plugin options', t => { '--address', 'fastify.dev:9999', '--socket', 'fastify.dev.socket:9999', '--require', './require-module.js', + '--import', './import-module.js', '--log-level', 'info', '--pretty-logs', 'true', '--watch', 'true', @@ -256,6 +265,7 @@ test('should parse custom plugin options', t => { address: 'fastify.dev:9999', socket: 'fastify.dev.socket:9999', require: './require-module.js', + import: './import-module.js', logLevel: 'info', prefix: 'FASTIFY_', pluginTimeout: 500, @@ -307,6 +317,7 @@ test('should parse config file correctly and prefer config values over default o address: 'fastify.dev:9999', socket: undefined, require: undefined, + import: undefined, prefix: 'FASTIFY_', loggingModule: undefined, lang: 'js', @@ -349,6 +360,7 @@ test('should prefer command line args over config file options', t => { address: 'fastify.dev:9999', socket: undefined, require: undefined, + import: undefined, prefix: 'FASTIFY_', loggingModule: undefined, lang: 'js', diff --git a/test/data/custom-import.mjs b/test/data/custom-import.mjs new file mode 100644 index 00000000..9dd35c4c --- /dev/null +++ b/test/data/custom-import.mjs @@ -0,0 +1,3 @@ +/* global GLOBAL_MODULE_3 */ +globalThis.GLOBAL_MODULE_3 = true // eslint-disable-line +console.log('this is module to be preloaded that sets GLOBAL_MODULE_3=', GLOBAL_MODULE_3) diff --git a/test/data/custom-import2.mjs b/test/data/custom-import2.mjs new file mode 100644 index 00000000..e3e2a2dd --- /dev/null +++ b/test/data/custom-import2.mjs @@ -0,0 +1,3 @@ +/* global GLOBAL_MODULE_4 */ +globalThis.GLOBAL_MODULE_4 = true // eslint-disable-line +console.log('this is module to be preloaded that sets GLOBAL_MODULE_4=', GLOBAL_MODULE_4) diff --git a/test/start.test.js b/test/start.test.js index 13f4cf69..fca1a129 100644 --- a/test/start.test.js +++ b/test/start.test.js @@ -1,4 +1,4 @@ -/* global GLOBAL_MODULE_1, GLOBAL_MODULE_2 */ +/* global GLOBAL_MODULE_1, GLOBAL_MODULE_2, GLOBAL_MODULE_3, GLOBAL_MODULE_4 */ 'use strict' const util = require('node:util') @@ -774,6 +774,17 @@ test('should support preloading custom module', async t => { t.pass('server closed') }) +test('should support preloading custom ES module', async t => { + t.plan(2) + + const argv = ['-i', './test/data/custom-import.mjs', './examples/plugin.js'] + const fastify = await start.start(argv) + t.ok(globalThis.GLOBAL_MODULE_3) + + await fastify.close() + t.pass('server closed') +}) + test('should support preloading multiple custom modules', async t => { t.plan(3) @@ -786,6 +797,17 @@ test('should support preloading multiple custom modules', async t => { t.pass('server closed') }) +test('should support preloading multiple custom ES modules', async t => { + t.plan(3) + + const argv = ['-i', './test/data/custom-import.mjs', '-i', './test/data/custom-import2.mjs', './examples/plugin.js'] + const fastify = await start.start(argv) + t.ok(GLOBAL_MODULE_3) + t.ok(GLOBAL_MODULE_4) + await fastify.close() + t.pass('server closed') +}) + test('preloading custom module with empty and trailing require flags should not throw', async t => { t.plan(2) @@ -797,6 +819,17 @@ test('preloading custom module with empty and trailing require flags should not t.pass('server closed') }) +test('preloading custom ES module with empty and trailing import flags should not throw', async t => { + t.plan(2) + + const argv = ['-i', './test/data/custom-import.mjs', '-i', '', './examples/plugin.js', '-i'] + const fastify = await start.start(argv) + t.ok(GLOBAL_MODULE_3) + + await fastify.close() + t.pass('server closed') +}) + test('preloading custom module that is not found should throw', async t => { t.plan(2) @@ -813,6 +846,22 @@ test('preloading custom module that is not found should throw', async t => { t.pass('server closed') }) +test('preloading custom ES module that is not found should throw', async t => { + t.plan(2) + + const oldStop = start.stop + t.teardown(() => { start.stop = oldStop }) + start.stop = function (err) { + t.ok(/Cannot find module/.test(err.message), err.message) + } + + const argv = ['-i', './test/data/import-missing.mjs', './examples/plugin.js'] + const fastify = await start.start(argv) + + await fastify.close() + t.pass('server closed') +}) + test('preloading custom module should be done before starting server', async t => { t.plan(4) diff --git a/util.js b/util.js index f8254283..74f09717 100644 --- a/util.js +++ b/util.js @@ -30,6 +30,15 @@ function requireModule (moduleName) { } } +async function requireESModule (moduleName) { + if (fs.existsSync(moduleName)) { + const moduleFilePath = path.resolve(moduleName) + return import(url.pathToFileURL(moduleFilePath)) + } else { + return import(moduleName) + } +} + function requireFastifyForModule (modulePath) { try { const basedir = path.resolve(process.cwd(), modulePath) @@ -104,4 +113,12 @@ function isKubernetes () { fs.existsSync('/run/secrets/kubernetes.io/serviceaccount/token') } -module.exports = { isKubernetes, exit, requireModule, requireFastifyForModule, showHelpForCommand, requireServerPluginFromPath } +module.exports = { + isKubernetes, + exit, + requireModule, + requireESModule, + requireFastifyForModule, + showHelpForCommand, + requireServerPluginFromPath +}