From 110853f6328dda762f4ee556c212b2a7c0949478 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Wed, 1 Apr 2026 16:43:06 +0900 Subject: [PATCH] feat: Allow the use of project-level external config file for js-compute-runtime CLI behavior --- README.md | 102 ++++++++++++++++++++++++++++++ package-lock.json | 61 +++++++++++------- package.json | 1 + src/cli/js-compute-runtime-cli.ts | 5 +- src/config.ts | 79 +++++++++++++++++++++++ 5 files changed, 223 insertions(+), 25 deletions(-) create mode 100644 src/config.ts diff --git a/README.md b/README.md index 167d0c0fea..5454ccd259 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,108 @@ addEventListener("fetch", event => { }); ``` +### CLI Flags and configuration + +The CLI is typically invoked by the `build` script defined in your project's `package.json` scripts. + +```json +{ + "scripts": { + "build": "js-compute-runtime src/index.js bin/main.wasm" + } +} +``` + +The CLI is invoked as follows: + +```sh +js-compute-runtime [OPTIONS] +``` + +Options can be specified on the command line or added to a persistent configuration file for the project. The CLI will search for a configuration starting from the current directory in the following order: + +* A `fastlycompute` property in `package.json` +* `.fastlycomputerc.json` +* `fastlycompute.config.js` + +
+Click here to see full list + +The CLI will search for a configuration starting from the current directory in the following order: + +- A `fastlycompute` property in `package.json` +- `.fastlycomputerc` (JSON or YAML) +- `.fastlycomputerc.json`, `.fastlycomputerc.yaml`, `.fastlycomputerc.yml` +- `.fastlycomputerc.js`, `.fastlycomputerc.mjs` +- `fastlycompute.config.js`, `fastlycompute.config.mjs` + +
+ +If an option is defined in both the command line and the configuration file, the command line option takes precedence. + +#### Supported Options + +| Config Key | CLI Flag | Type | Description | +|:----------------------------------------------|:-----------------------------------------------------|:----------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `enableAOT` | `--enable-aot` | `boolean` | Enable AOT compilation for performance | +| `aotCache` | `--aot-cache` | `string` (path) | Specify a path to the AOT cache file | +| `enableHttpCache` | `--enable-http-cache` | `boolean` | Enable the [HTTP cache hook API](https://www.fastly.com/documentation/guides/concepts/cache/#modifying-a-request-as-it-is-forwarded-to-a-backend) | +| `enableExperimentalHighResolutionTimeMethods` | `--enable-experimental-high-resolution-time-methods` | `boolean` | Enable experimental fastly.now() method | +| `enableExperimentalTopLevelAwait` | `--enable-experimental-top-level-await` | `boolean` | Enable experimental top level await | +| `enableStackTraces` | `--enable-stack-traces` | `boolean` | Enable stack traces | +| `excludeSources` | `--exclude-sources` | `boolean` | Don't include sources in stack traces | +| `debugIntermediateFiles` | `--debug-intermediate-files` | `string` (path) | Output intermediate files in directory | +| `debugBuild` | `--debug-build` | `boolean` | Use debug build of the SDK runtime | +| `engineWasm` | `--engine-wasm` | `string` (path) | Specify a custom Wasm engine (advanced) | +| `wevalBin` | `--weval-bin` | `string` (path) | Specify a custom weval binary (advanced) | +| `env` | `--env` | `string \| object \| array` | Set environment variables, possibly inheriting from the current environment. Multiple variables can be comma-separated (e.g., --env ENV_VAR,OVERRIDE=val) | + +NOTE: The `env` field is additive. Values defined on the command-line values append to, rather than replace, the values defined in any configuration file. + +#### Example command-line options + +```sh +js-compute-runtime --enable-aot --enable-stack-traces --enable-top-level-await --env=LOG_LEVEL=debug ./src/index.js ./bin/main.wasm +``` + +#### Example `.fastlycomputerc.json` + +```json +{ + "enableAOT": true, + "enableStackTraces": true, + "enableTopLevelAwait": true, + "env": { + "LOG_LEVEL": "debug" + } +} +``` + +#### Example `package.json` + +```json +{ + "name": "my-fastly-service", + "type": "module", + "dependencies": { + "@fastly/cli": "^13.0.0" + }, + "scripts": { + "build": "js-compute-runtime ./src/index.js ./bin/main.wasm", + "dev": "fastly compute serve", + "deploy": "fastly compute publish" + }, + "fastlycompute": { + "enableAOT": true, + "enableStackTraces": true, + "enableTopLevelAwait": true, + "env": { + "LOG_LEVEL": "debug" + } + } +} +``` + ### API documentation The API documentation for the JavaScript SDK is located at [https://js-compute-reference-docs.edgecompute.app](https://js-compute-reference-docs.edgecompute.app). diff --git a/package-lock.json b/package-lock.json index 5538be7b05..0015b7d089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@jridgewell/trace-mapping": "^0.3.31", "acorn": "^8.13.0", "acorn-walk": "^8.3.4", + "cosmiconfig": "^9.0.1", "esbuild": "^0.25.0", "magic-string": "^0.30.12", "picomatch": "^4.0.3", @@ -54,7 +55,6 @@ "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/highlight": "^7.25.7", @@ -68,7 +68,6 @@ "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -78,7 +77,6 @@ "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.7", @@ -94,7 +92,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -107,7 +104,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -122,7 +118,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -132,7 +127,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -142,7 +136,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -1883,7 +1876,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-union": { @@ -2101,7 +2093,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2204,7 +2195,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -2214,7 +2204,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -2246,6 +2235,32 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2495,11 +2510,19 @@ "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -3329,7 +3352,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -3382,7 +3404,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-core-module": { @@ -3644,14 +3665,12 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3683,7 +3702,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -3745,7 +3763,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -4706,7 +4723,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -4719,7 +4735,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -4781,7 +4796,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5220,7 +5234,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -5764,7 +5777,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index a257751c93..e3d42770f5 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@jridgewell/trace-mapping": "^0.3.31", "acorn": "^8.13.0", "acorn-walk": "^8.3.4", + "cosmiconfig": "^9.0.1", "esbuild": "^0.25.0", "magic-string": "^0.30.12", "picomatch": "^4.0.3", diff --git a/src/cli/js-compute-runtime-cli.ts b/src/cli/js-compute-runtime-cli.ts index 4ec6f1f88f..e7570a7c8e 100755 --- a/src/cli/js-compute-runtime-cli.ts +++ b/src/cli/js-compute-runtime-cli.ts @@ -3,8 +3,11 @@ import { parseInputs } from '../parseInputs.js'; import { printHelp, printVersion } from '../printHelp.js'; import { addSdkMetadataField } from '../addSdkMetadataField.js'; +import { readConfigFileAndCliArguments } from '../config.js'; -const parsedInputs = await parseInputs(process.argv.slice(2)); +const argv = await readConfigFileAndCliArguments(process.argv.slice(2)); + +const parsedInputs = await parseInputs(argv); if (parsedInputs === 'version') { await printVersion(); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000000..b244d632eb --- /dev/null +++ b/src/config.ts @@ -0,0 +1,79 @@ +import { cosmiconfig } from 'cosmiconfig'; + +const additiveOptionsMap = { + env: '--env', +}; + +const strictOptionsMap = { + enableAOT: '--enable-aot', + aotCache: '--aot-cache', + enableHttpCache: '--enable-http-cache', + enableExperimentalHighResolutionTimeMethods: + '--enable-experimental-high-resolution-time-methods', + enableExperimentalTopLevelAwait: '--enable-experimental-top-level-await', + enableStackTraces: '--enable-stack-traces', + excludeSources: '--exclude-sources', + debugIntermediateFiles: '--debug-intermediate-files', + debugBuild: '--debug-build', + engineWasm: '--engine-wasm', + wevalBin: '--weval-bin', +}; + +export async function readConfigFileAndCliArguments(cliArgs: string[]) { + const explorer = cosmiconfig('fastlycompute'); + const result = await explorer.search(); + if (!result?.config) { + return cliArgs; + } + + const config = result.config as Record< + string, + string | boolean | object | (string | boolean | object)[] + >; + const synthesizedArgs: string[] = []; + + // --- Loop 1: Additive Options (Array-Normalized) --- + for (const [configKey, flag] of Object.entries(additiveOptionsMap)) { + const val = config[configKey]; + if (val === undefined || val === null) continue; + + // Wrap in an array if it isn't one already + const items = (Array.isArray(val) ? val : [val]) as ( + | string + | boolean + | object + )[]; + + for (const item of items) { + if (typeof item === 'object' && item !== null) { + // Handle: { "FOO": "bar" } -> --env FOO=bar + for (const [k, v] of Object.entries(item)) { + synthesizedArgs.push(flag, `${k}=${v}`); + } + } else { + // Handle: "A,B" -> --env A,B + synthesizedArgs.push(flag, String(item)); + } + } + } + + // --- Loop 2: Strict Options (Override Check) --- + for (const [configKey, flag] of Object.entries(strictOptionsMap)) { + const val = config[configKey]; + if (val === undefined || val === null) continue; + + const isOverridden = cliArgs.some( + (arg) => arg === flag || arg.startsWith(`${flag}=`), + ); + + if (!isOverridden) { + if (typeof val === 'boolean' && val) { + synthesizedArgs.push(flag); + } else if (typeof val === 'string' || typeof val === 'number') { + synthesizedArgs.push(flag, String(val)); + } + } + } + + return [...synthesizedArgs, ...cliArgs]; +}