heximon 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/bin.mjs +1676 -40
- package/package.json +17 -7
package/dist/bin.mjs
CHANGED
|
@@ -3,18 +3,19 @@ import { c as RegistryAccess, d as ClackPrompter, f as PromptCancelledError, l a
|
|
|
3
3
|
import { createRequire, register } from "node:module";
|
|
4
4
|
import { cancel, confirm, intro, isCancel, log, outro, select, spinner } from "@clack/prompts";
|
|
5
5
|
import process from "node:process";
|
|
6
|
-
import {
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
7
|
import { defineCommand, runMain } from "citty";
|
|
8
|
-
import { basename, dirname, join, relative, resolve } from "pathe";
|
|
8
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "pathe";
|
|
9
9
|
import tab from "@bomb.sh/tab/citty";
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { CompileMode, DiagnosticCode, DiagnosticRenderMode, DiagnosticRenderer, DiagnosticSeverity, RuntimeSingletons, TerminalStyle, UpgradeBaselineFile } from "@heximon/compiler";
|
|
12
|
-
import { ConfigLoader, LibraryEmitter, ProjectCompiler, RuntimeConfigLoader } from "@heximon/compiler/project";
|
|
10
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { AstNodeType, CompileEnvironment, CompileMode, DeployPlatform, DiagnosticCode, DiagnosticRenderMode, DiagnosticRenderer, DiagnosticSeverity, ImportKind, ReexportIndex, RuntimeConfigFile, RuntimeSingletons, TerminalStyle, UpgradeBaselineFile } from "@heximon/compiler";
|
|
12
|
+
import { ConfigLoader, LibraryEmitter, LibraryFactoryPreflight, ProjectCompiler, RuntimeConfigLoader, SourceModuleResolution, SourceParser, WorkspaceTopologyReader } from "@heximon/compiler/project";
|
|
13
13
|
import { NodeCompileSeam } from "@heximon/node/compiler";
|
|
14
14
|
import { TransactionCensus, TransactionCensusPolicy } from "@heximon/cqrs/compiler";
|
|
15
15
|
import { BoundaryProvenanceAuditor, BoundaryProvenanceStatus, CredentialSelection, DatabaseConfigDetector, DeployDiagnosticSeverity, DeployLedger, DeployManifestStore, DeployRunner, DeployStrategyUnavailableError, DeployTargetResolver, DeployableServiceReader, DotenvCascade, DrizzleKitRunner, EnvFileReader, EnvFileUnreadableError, HostManagedDeployUnsupportedError, ImportScanner, MigrationInspector, MigrationTargetComposer, PackageCopyAuditor, PeerClosureAuditor, PlatformTracingAuditor, PlatformTracingStatus, WorkspaceConsumerLocator } from "@heximon/deploy";
|
|
16
16
|
import { RouteCensus, RouteScopeGap } from "@heximon/http/compiler";
|
|
17
17
|
import { pathToFileURL } from "node:url";
|
|
18
|
+
import { homedir } from "node:os";
|
|
18
19
|
//#region src/compile-diagnostics.ts
|
|
19
20
|
/**
|
|
20
21
|
* Renders compile diagnostics to stderr with the same codeframe layout the dev server shows — a box-drawing
|
|
@@ -123,6 +124,8 @@ var ProjectCompileSession = class ProjectCompileSession {
|
|
|
123
124
|
/** The compiler plugins the sidecar declares, merged with the platform strategy's own. */
|
|
124
125
|
plugins;
|
|
125
126
|
strategy;
|
|
127
|
+
/** The `$env` name the sidecar was read for, which the member sidecars of the workspace scan share. */
|
|
128
|
+
environment;
|
|
126
129
|
/** The runtime config file the app compile checks and bakes in, or `undefined` when there is none. */
|
|
127
130
|
runtimeConfig;
|
|
128
131
|
constructor(options) {
|
|
@@ -133,6 +136,7 @@ var ProjectCompileSession = class ProjectCompileSession {
|
|
|
133
136
|
this.sourceDirectories = options.sourceDirectories;
|
|
134
137
|
this.plugins = options.plugins;
|
|
135
138
|
this.strategy = options.strategy;
|
|
139
|
+
this.environment = options.environment;
|
|
136
140
|
this.outputDirectory = join(options.rootDir, ProjectCompileSession.outputDirectoryName);
|
|
137
141
|
}
|
|
138
142
|
/**
|
|
@@ -151,7 +155,8 @@ var ProjectCompileSession = class ProjectCompileSession {
|
|
|
151
155
|
runtimeConfig: await RuntimeConfigLoader.load(options.rootDir, config, options.environment),
|
|
152
156
|
sourceDirectories: options.sourceDirectories,
|
|
153
157
|
plugins: NodeCompileSeam.mergePlugins(config?.plugins ?? [], strategy),
|
|
154
|
-
strategy
|
|
158
|
+
strategy,
|
|
159
|
+
environment: options.environment
|
|
155
160
|
});
|
|
156
161
|
}
|
|
157
162
|
/**
|
|
@@ -176,6 +181,42 @@ var ProjectCompileSession = class ProjectCompileSession {
|
|
|
176
181
|
...options.verifyBaseline === true ? { verifyBaseline: true } : {}
|
|
177
182
|
});
|
|
178
183
|
}
|
|
184
|
+
/** The platform the session compiles for, or `undefined` for the zero-config Node default. */
|
|
185
|
+
platformName() {
|
|
186
|
+
return this.strategy.platformName;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Run the compile `vp build` runs, into a chosen directory: the production graph with the sidecar's wiring
|
|
190
|
+
* mode, diagnostic dials and cross-service topology, which {@link compile} leaves out.
|
|
191
|
+
*
|
|
192
|
+
* @param outputDirectory - The directory the generated files will be written to. It must sit directly
|
|
193
|
+
* under the project root, because the generated imports of the app source are relative to it.
|
|
194
|
+
* @param aliases - The module aliases of the project (prefix → absolute path), when it has some.
|
|
195
|
+
* @returns The compile output. Nothing is written.
|
|
196
|
+
*/
|
|
197
|
+
async compileProduction(outputDirectory, aliases = {
|
|
198
|
+
exact: {},
|
|
199
|
+
prefixes: {}
|
|
200
|
+
}) {
|
|
201
|
+
const workspaceSubscribers = await WorkspaceTopologyReader.resolveFromConfig(this.rootDir, this.plugins, this.config, this.environment);
|
|
202
|
+
return this.compiler.compile({
|
|
203
|
+
rootDir: this.rootDir,
|
|
204
|
+
outputDirectory,
|
|
205
|
+
plugins: this.plugins,
|
|
206
|
+
strategy: this.strategy,
|
|
207
|
+
environment: CompileEnvironment.Production,
|
|
208
|
+
aliases: aliases.prefixes,
|
|
209
|
+
exactAliases: aliases.exact,
|
|
210
|
+
...this.sourceDirectories === void 0 ? {} : { sourceDirectories: this.sourceDirectories },
|
|
211
|
+
...this.config === void 0 ? {} : { config: this.config },
|
|
212
|
+
...this.config?.wiringMode === void 0 ? {} : { wiringMode: this.config.wiringMode },
|
|
213
|
+
...this.config?.strictDiagnostics === void 0 ? {} : { strictDiagnostics: this.config.strictDiagnostics },
|
|
214
|
+
...this.config?.suppressDiagnostics === void 0 ? {} : { suppressDiagnostics: this.config.suppressDiagnostics },
|
|
215
|
+
...this.runtimeConfig === void 0 ? {} : { runtimeConfig: this.runtimeConfig },
|
|
216
|
+
...this.config?.root === void 0 ? {} : { apps: { main: this.config.root } },
|
|
217
|
+
...workspaceSubscribers === void 0 ? {} : { workspaceSubscribers }
|
|
218
|
+
});
|
|
219
|
+
}
|
|
179
220
|
/**
|
|
180
221
|
* Run the library-mode compile — a published module package goes through the same {@link LibraryEmitter}
|
|
181
222
|
* its own `vp pack` build uses, rather than being compiled as an app. Not a byte-for-byte replay of that
|
|
@@ -1337,7 +1378,7 @@ var DoctorReport = class DoctorReport {
|
|
|
1337
1378
|
* ({@link PackageCopyAuditor.installedBoundaryPackages}) — exits non-zero; a fork confined to build-time
|
|
1338
1379
|
* tooling (e.g. a devDependency-only bundler) is reported and does not gate.
|
|
1339
1380
|
* 3. the staleness check ({@link BoundaryProvenanceAuditor.auditSelf}) — this package's own
|
|
1340
|
-
* `dist/heximon.manifest.json` against its own
|
|
1381
|
+
* `dist/heximon.manifest.json` against its own source directory. Stale exits non-zero.
|
|
1341
1382
|
*
|
|
1342
1383
|
* Compile-free and filesystem-local (the `heximon.config.ts` sidecar is loaded only to read
|
|
1343
1384
|
* `expectedOpenPeers`); exits non-zero when this package forks a runtime-reachable package against a
|
|
@@ -1378,7 +1419,7 @@ const doctorCommand = defineCommand({
|
|
|
1378
1419
|
let forked = false;
|
|
1379
1420
|
if (workspaceConsumers === null) log.info("Package copies — no pnpm workspace found from this directory; census not run.");
|
|
1380
1421
|
else forked = DoctorReport.logCopyAudit(PackageCopyAuditor.auditLibrary(rootDir, workspaceConsumers.consumers), DoctorReport.runtimeReachablePackageNames(rootDir, closureReport));
|
|
1381
|
-
const selfReport = BoundaryProvenanceAuditor.auditSelf(rootDir);
|
|
1422
|
+
const selfReport = await BoundaryProvenanceAuditor.auditSelf(rootDir);
|
|
1382
1423
|
const outroParts = [];
|
|
1383
1424
|
let exitFailure = forked || registryFailed;
|
|
1384
1425
|
if (forked) {
|
|
@@ -2137,66 +2178,150 @@ var RouteTableReport = class RouteTableReport {
|
|
|
2137
2178
|
}
|
|
2138
2179
|
};
|
|
2139
2180
|
//#endregion
|
|
2140
|
-
//#region src/
|
|
2181
|
+
//#region src/transform-boot.ts
|
|
2141
2182
|
/**
|
|
2142
|
-
*
|
|
2143
|
-
*
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
*
|
|
2183
|
+
* Runs a command that imports an app's TypeScript source in-process (`routes --boot`, `task run`).
|
|
2184
|
+
* The generated wiring imports the source through `.js` specifiers, and Heximon's parameter-property
|
|
2185
|
+
* classes need a TypeScript TRANSFORM (not just type-stripping), so such a command runs under a one-time
|
|
2186
|
+
* re-exec with Node's transform-types enabled plus a resolve hook that maps those `.js` specifiers to their
|
|
2187
|
+
* `.ts` sources.
|
|
2147
2188
|
*/
|
|
2148
|
-
var
|
|
2189
|
+
var TransformBoot = class TransformBoot {
|
|
2149
2190
|
/** Set on the re-exec'd child so it boots rather than re-exec'ing again. */
|
|
2150
2191
|
static transformMarker = "HEXIMON_ROUTES_TRANSFORM";
|
|
2192
|
+
/** The signals that reach only the parent, and so are passed on to the child. */
|
|
2193
|
+
static forwardedSignals = ["SIGTERM", "SIGHUP"];
|
|
2151
2194
|
/** The `.js`/extensionless → `.ts` ESM resolve hook, inlined as a `data:` module so it ships in no file. */
|
|
2152
|
-
static sourceResolverModule =
|
|
2195
|
+
static sourceResolverModule = TransformBoot.buildSourceResolverModule();
|
|
2153
2196
|
/** Whether this process is the transform-enabled child that performs the boot. */
|
|
2154
2197
|
static isBootProcess() {
|
|
2155
|
-
return process.env[
|
|
2198
|
+
return process.env[TransformBoot.transformMarker] === "1";
|
|
2156
2199
|
}
|
|
2157
2200
|
/**
|
|
2158
2201
|
* Re-run this exact CLI invocation under `node --experimental-transform-types`, inheriting stdio so the
|
|
2159
2202
|
* child's output and prompts reach the terminal directly.
|
|
2160
2203
|
*
|
|
2204
|
+
* The child owns an interrupt: a terminal sends Ctrl-C to both processes, and the child cleans up. A
|
|
2205
|
+
* `SIGTERM` or `SIGHUP` that reaches only this process is passed on, so the child never outlives it.
|
|
2206
|
+
*
|
|
2161
2207
|
* A child that never started, or that a signal killed, reports no exit code. Both become `1` — but they
|
|
2162
2208
|
* are NAMED on stderr first, because the child printed nothing of its own and a bare `exit 1` after a
|
|
2163
2209
|
* silent run reads as "the command found a problem" rather than "the command never ran".
|
|
2164
2210
|
*
|
|
2211
|
+
* @param commandLabel - The command name for the failure messages (`routes --boot`, `task run`).
|
|
2165
2212
|
* @returns The child process exit code (1 when it did not report one).
|
|
2166
2213
|
*/
|
|
2167
|
-
static reexecUnderTransform() {
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2214
|
+
static reexecUnderTransform(commandLabel) {
|
|
2215
|
+
return new Promise((resolve) => {
|
|
2216
|
+
const child = spawn(process.execPath, [
|
|
2217
|
+
"--experimental-transform-types",
|
|
2218
|
+
"--disable-warning=ExperimentalWarning",
|
|
2219
|
+
process.argv[1] ?? "",
|
|
2220
|
+
...process.argv.slice(2)
|
|
2221
|
+
], {
|
|
2222
|
+
stdio: "inherit",
|
|
2223
|
+
env: {
|
|
2224
|
+
...process.env,
|
|
2225
|
+
[TransformBoot.transformMarker]: "1"
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
const ignoreInterrupt = () => void 0;
|
|
2229
|
+
const forwarded = TransformBoot.forwardedSignals.map((signal) => {
|
|
2230
|
+
const forward = () => {
|
|
2231
|
+
child.kill(signal);
|
|
2232
|
+
};
|
|
2233
|
+
process.on(signal, forward);
|
|
2234
|
+
return [signal, forward];
|
|
2235
|
+
});
|
|
2236
|
+
process.on("SIGINT", ignoreInterrupt);
|
|
2237
|
+
const finish = (code) => {
|
|
2238
|
+
process.off("SIGINT", ignoreInterrupt);
|
|
2239
|
+
for (const [signal, forward] of forwarded) process.off(signal, forward);
|
|
2240
|
+
resolve(code);
|
|
2241
|
+
};
|
|
2242
|
+
child.once("error", (error) => {
|
|
2243
|
+
process.stderr.write(`heximon ${commandLabel}: could not start the transform-enabled child (${process.execPath}): ${error.message}\n`);
|
|
2244
|
+
finish(1);
|
|
2245
|
+
});
|
|
2246
|
+
child.once("close", (code, signal) => {
|
|
2247
|
+
if (signal !== null) process.stderr.write(`heximon ${commandLabel}: the transform-enabled child was killed by ${signal}.\n`);
|
|
2248
|
+
finish(code ?? 1);
|
|
2249
|
+
});
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Install the source hooks so the imminent boot can import the app's `.ts` source.
|
|
2254
|
+
*
|
|
2255
|
+
* @param rewrites - Source text to serve instead of what is on disk, by absolute file path. A production
|
|
2256
|
+
* build applies these rewrites, which drop the imports of dev-only modules, so a production run applies them too.
|
|
2257
|
+
* @param aliases - The module aliases of the project. An exact alias (`@app` → one file) matches that specifier
|
|
2258
|
+
* only. A prefix alias (`@users` → a directory) matches `@users/<rest>` on whole path segments, the longest
|
|
2259
|
+
* prefix first. An exact alias wins over a prefix. This is the precedence of the compile and of TypeScript.
|
|
2260
|
+
*/
|
|
2261
|
+
static registerSourceResolver(rewrites = /* @__PURE__ */ new Map(), aliases = {
|
|
2262
|
+
exact: {},
|
|
2263
|
+
prefixes: {}
|
|
2264
|
+
}) {
|
|
2265
|
+
register(TransformBoot.sourceResolverModule, {
|
|
2266
|
+
parentURL: import.meta.url,
|
|
2267
|
+
data: {
|
|
2268
|
+
rewrites: [...rewrites].map(([filePath, content]) => [pathToFileURL(TransformBoot.realPath(filePath)).href, content]),
|
|
2269
|
+
exact: Object.entries(aliases.exact),
|
|
2270
|
+
aliases: Object.entries(aliases.prefixes),
|
|
2271
|
+
compilerUrl: import.meta.resolve("@heximon/compiler/project")
|
|
2178
2272
|
}
|
|
2179
2273
|
});
|
|
2180
|
-
if (result.error !== void 0) process.stderr.write(`heximon routes --boot: could not start the transform-enabled child (${process.execPath}): ${result.error.message}\n`);
|
|
2181
|
-
else if (result.signal !== null && result.signal !== void 0) process.stderr.write(`heximon routes --boot: the transform-enabled child was killed by ${result.signal}.\n`);
|
|
2182
|
-
return result.status ?? 1;
|
|
2183
2274
|
}
|
|
2184
|
-
/**
|
|
2185
|
-
static
|
|
2186
|
-
|
|
2275
|
+
/** The real path of `path`, or `path` itself when it does not exist. */
|
|
2276
|
+
static realPath(path) {
|
|
2277
|
+
try {
|
|
2278
|
+
return realpathSync(path);
|
|
2279
|
+
} catch {
|
|
2280
|
+
return path;
|
|
2281
|
+
}
|
|
2187
2282
|
}
|
|
2188
2283
|
/** Build the inline resolve-hook module — mirrors the compiler's own relative-import resolution. */
|
|
2189
2284
|
static buildSourceResolverModule() {
|
|
2190
2285
|
const source = [
|
|
2191
|
-
"import { existsSync } from \"node:fs\";",
|
|
2286
|
+
"import { existsSync, realpathSync } from \"node:fs\";",
|
|
2192
2287
|
"import { fileURLToPath, pathToFileURL } from \"node:url\";",
|
|
2288
|
+
"let rewrites = new Map();",
|
|
2289
|
+
"let resolver;",
|
|
2290
|
+
"export async function initialize(data) {",
|
|
2291
|
+
" rewrites = new Map(data.rewrites);",
|
|
2292
|
+
" // The alias rule is the compiler's own, so the boot and the compile agree on every specifier.",
|
|
2293
|
+
" if (data.aliases.length > 0 || data.exact.length > 0) {",
|
|
2294
|
+
" const compiler = await import(data.compilerUrl);",
|
|
2295
|
+
" resolver = new compiler.AliasModuleResolver(Object.fromEntries(data.aliases), undefined, Object.fromEntries(data.exact));",
|
|
2296
|
+
" }",
|
|
2297
|
+
"}",
|
|
2298
|
+
"export async function load(url, context, nextLoad) {",
|
|
2299
|
+
" const result = await nextLoad(url, context);",
|
|
2300
|
+
" const content = rewrites.get(url);",
|
|
2301
|
+
" return content === undefined ? result : { ...result, source: content };",
|
|
2302
|
+
"}",
|
|
2193
2303
|
"export async function resolve(specifier, context, nextResolve) {",
|
|
2194
2304
|
" if ((specifier.startsWith(\"./\") || specifier.startsWith(\"../\")) && context.parentURL) {",
|
|
2195
2305
|
" const base = fileURLToPath(new URL(specifier, context.parentURL));",
|
|
2196
2306
|
" const withoutJs = base.endsWith(\".js\") ? base.slice(0, -3) : base;",
|
|
2197
2307
|
" for (const candidate of [withoutJs + \".ts\", base + \".ts\", base + \"/index.ts\"]) {",
|
|
2198
2308
|
" if (candidate.endsWith(\".ts\") && existsSync(candidate)) {",
|
|
2199
|
-
" return { url: pathToFileURL(candidate).href, shortCircuit: true };",
|
|
2309
|
+
" return { url: pathToFileURL(realpathSync(candidate)).href, shortCircuit: true };",
|
|
2310
|
+
" }",
|
|
2311
|
+
" }",
|
|
2312
|
+
" }",
|
|
2313
|
+
" if (resolver !== undefined) {",
|
|
2314
|
+
" const resolution = resolver.explain(specifier);",
|
|
2315
|
+
" if (resolution.kind === \"scanned\" || resolution.kind === \"outside-scan\") {",
|
|
2316
|
+
" return { url: pathToFileURL(realpathSync(resolution.filePath)).href, shortCircuit: true };",
|
|
2317
|
+
" }",
|
|
2318
|
+
" if (resolution.kind === \"no-source-file\") {",
|
|
2319
|
+
" // The compile finds no file here either and leaves the specifier to the normal resolution, so an",
|
|
2320
|
+
" // installed package of the same name still loads. The alias is named only when that fails too.",
|
|
2321
|
+
" try {",
|
|
2322
|
+
" return await nextResolve(specifier, context);",
|
|
2323
|
+
" } catch (error) {",
|
|
2324
|
+
" throw new Error('The alias \"' + resolution.prefix + '\" does not resolve \"' + specifier + '\": no file exists at ' + resolution.base + '.', { cause: error });",
|
|
2200
2325
|
" }",
|
|
2201
2326
|
" }",
|
|
2202
2327
|
" }",
|
|
@@ -2206,6 +2331,8 @@ var RoutesBoot = class RoutesBoot {
|
|
|
2206
2331
|
return `data:text/javascript,${encodeURIComponent(source)}`;
|
|
2207
2332
|
}
|
|
2208
2333
|
};
|
|
2334
|
+
//#endregion
|
|
2335
|
+
//#region src/routes-command.ts
|
|
2209
2336
|
/**
|
|
2210
2337
|
* `heximon routes` — print the app's unified HTTP route table (inline-mode AND contract-mode routes
|
|
2211
2338
|
* together), each row carrying the authorization scopes the app enforces on it.
|
|
@@ -2263,8 +2390,8 @@ const routesCommand = defineCommand({
|
|
|
2263
2390
|
},
|
|
2264
2391
|
async run({ args }) {
|
|
2265
2392
|
const asBoot = args.boot;
|
|
2266
|
-
if (asBoot && !
|
|
2267
|
-
process.exitCode =
|
|
2393
|
+
if (asBoot && !TransformBoot.isBootProcess()) {
|
|
2394
|
+
process.exitCode = await TransformBoot.reexecUnderTransform("routes --boot");
|
|
2268
2395
|
return;
|
|
2269
2396
|
}
|
|
2270
2397
|
const rootDir = resolve(process.cwd(), args.cwd);
|
|
@@ -2341,7 +2468,7 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2341
2468
|
let app;
|
|
2342
2469
|
let inventory;
|
|
2343
2470
|
try {
|
|
2344
|
-
|
|
2471
|
+
TransformBoot.registerSourceResolver();
|
|
2345
2472
|
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
2346
2473
|
DotenvCascade.loadInto(rootDir, args.env);
|
|
2347
2474
|
const config = await ConfigLoader.load(rootDir, args.env);
|
|
@@ -2455,6 +2582,1514 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2455
2582
|
}
|
|
2456
2583
|
};
|
|
2457
2584
|
//#endregion
|
|
2585
|
+
//#region src/project-modules.ts
|
|
2586
|
+
/**
|
|
2587
|
+
* Imports packages the way the generated wiring imports them: through a small module written into the
|
|
2588
|
+
* compile output directory, which sits in the project. The specifiers inside it resolve from the project, so
|
|
2589
|
+
* the command gets the copy of a package that the wiring gets. A package the command imported from its own
|
|
2590
|
+
* install would be a second set of class objects, and `app.from(CoreModule)` would not know them.
|
|
2591
|
+
*/
|
|
2592
|
+
var ProjectModules = class ProjectModules {
|
|
2593
|
+
/**
|
|
2594
|
+
* Import `specifiers` of the project.
|
|
2595
|
+
*
|
|
2596
|
+
* @param directory - A directory inside the project, with write access (the compile output directory).
|
|
2597
|
+
* @param fileName - The name of the module to write, unique among the calls for one directory.
|
|
2598
|
+
* @param source - The module source: `export … from "<specifier>"` lines.
|
|
2599
|
+
* @returns The namespace of the written module.
|
|
2600
|
+
*/
|
|
2601
|
+
static async import(directory, fileName, source) {
|
|
2602
|
+
const file = join(directory, fileName);
|
|
2603
|
+
writeFileSync(file, source);
|
|
2604
|
+
return await import(pathToFileURL(file).href);
|
|
2605
|
+
}
|
|
2606
|
+
/** The runtime of the project: everything the command asks of `@heximon/runtime`. */
|
|
2607
|
+
static runtime(directory) {
|
|
2608
|
+
return ProjectModules.import(directory, "project-runtime.js", "export * from \"@heximon/runtime\";\n");
|
|
2609
|
+
}
|
|
2610
|
+
};
|
|
2611
|
+
//#endregion
|
|
2612
|
+
//#region src/nitro-host.ts
|
|
2613
|
+
/**
|
|
2614
|
+
* The production inputs of a Nitro host project, for the preset that was read from its config files. The
|
|
2615
|
+
* preset picks the deploy platform, and the compile adds the Nitro host plugin, so a compile that reads the
|
|
2616
|
+
* sidecar alone would build another graph. `@heximon/nitro` of the project makes the inputs from the preset
|
|
2617
|
+
* name. Both packages load from the project, so the plugins, the strategy and the compiler are one copy. No
|
|
2618
|
+
* Nitro or Vite config runs.
|
|
2619
|
+
*/
|
|
2620
|
+
var NitroHost = class NitroHost {
|
|
2621
|
+
context;
|
|
2622
|
+
aliases;
|
|
2623
|
+
constructor(context, aliases) {
|
|
2624
|
+
this.context = context;
|
|
2625
|
+
this.aliases = aliases;
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Resolve the inputs of the Nitro host.
|
|
2629
|
+
*
|
|
2630
|
+
* @param rootDir - The project root.
|
|
2631
|
+
* @param directory - A directory inside the project to write the import module into.
|
|
2632
|
+
* @param request - The preset (or the choice to take the platform from the sidecar), the literal source
|
|
2633
|
+
* directories of the Nitro config, and the module aliases of the project (the tsconfig `paths`).
|
|
2634
|
+
* @throws {Error} When `@heximon/nitro` is not installed in the project.
|
|
2635
|
+
*/
|
|
2636
|
+
static async open(rootDir, directory, request) {
|
|
2637
|
+
const modules = await ProjectModules.import(directory, "project-nitro.js", "export { resolveNitroPresetInputs } from \"@heximon/nitro/module\";\n");
|
|
2638
|
+
return new NitroHost(await modules.resolveNitroPresetInputs({
|
|
2639
|
+
rootDirectory: rootDir,
|
|
2640
|
+
preset: request.preset,
|
|
2641
|
+
aliases: request.aliases.prefixes,
|
|
2642
|
+
...request.serverDir === void 0 ? {} : { serverDir: request.serverDir },
|
|
2643
|
+
...request.scanDirs === void 0 ? {} : { scanDirs: request.scanDirs },
|
|
2644
|
+
...request.platformFromSidecar === true ? { platformFromSidecar: true } : {}
|
|
2645
|
+
}), request.aliases);
|
|
2646
|
+
}
|
|
2647
|
+
/** Whether the sidecar declares the deploy platform. */
|
|
2648
|
+
hasSidecarPlatform() {
|
|
2649
|
+
return this.context.config?.platform !== void 0;
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Run the compile the host's production build runs.
|
|
2653
|
+
*
|
|
2654
|
+
* @param rootDir - The project root.
|
|
2655
|
+
* @param outputDirectory - Where the generated files will be written.
|
|
2656
|
+
* @param sourceDirectories - The source directories to scan instead of the host's, when the caller has some.
|
|
2657
|
+
*/
|
|
2658
|
+
compile(rootDir, outputDirectory, sourceDirectories) {
|
|
2659
|
+
const context = this.context;
|
|
2660
|
+
const config = context.config;
|
|
2661
|
+
const compiler = new ProjectCompiler();
|
|
2662
|
+
return {
|
|
2663
|
+
output: compiler.compile({
|
|
2664
|
+
rootDir,
|
|
2665
|
+
outputDirectory,
|
|
2666
|
+
sourceDirectories: sourceDirectories ?? context.sourceDirectories,
|
|
2667
|
+
aliases: this.aliases.prefixes,
|
|
2668
|
+
exactAliases: this.aliases.exact,
|
|
2669
|
+
plugins: context.plugins,
|
|
2670
|
+
strategy: context.strategy,
|
|
2671
|
+
...context.wiringMode === void 0 ? {} : { wiringMode: context.wiringMode },
|
|
2672
|
+
...config === void 0 ? {} : { config },
|
|
2673
|
+
...context.runtimeConfig === void 0 ? {} : { runtimeConfig: context.runtimeConfig },
|
|
2674
|
+
...config?.root === void 0 ? {} : { apps: { main: config.root } },
|
|
2675
|
+
...context.workspaceSubscribers === void 0 ? {} : { workspaceSubscribers: context.workspaceSubscribers }
|
|
2676
|
+
}),
|
|
2677
|
+
compiler,
|
|
2678
|
+
platformName: context.strategy.platformName
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
//#endregion
|
|
2683
|
+
//#region src/nitro-static-config.ts
|
|
2684
|
+
/**
|
|
2685
|
+
* Reads the Nitro options that choose the graph of a build (`preset`, `serverDir`, `scanDirs`) from the source of
|
|
2686
|
+
* the config files, without running them. A config file is project code: it can read the environment, call a
|
|
2687
|
+
* service, or write a file. So the files are parsed with oxc, and the reader ACCEPTS only a shape that it can
|
|
2688
|
+
* prove. Everything else is "computed", and the command then refuses or asks for an explicit input.
|
|
2689
|
+
*
|
|
2690
|
+
* Accepted in `nitro.config.*`: `export default {…}`, or `export default defineNitroConfig({…})` with
|
|
2691
|
+
* `defineNitroConfig` imported from `nitro` or `nitro/config`, or either one through ONE top-level `const` that is
|
|
2692
|
+
* never assigned again. The object has no spread. A `$production` or `$env.production` overlay applies; the other
|
|
2693
|
+
* environments do not, because the command always compiles for production.
|
|
2694
|
+
*
|
|
2695
|
+
* Accepted in `vite.config.*`: ONE call of the Nitro plugin of `nitro/vite` (a named or a namespace import), as a
|
|
2696
|
+
* direct element of the `plugins` array literal of the exported config (`export default {…}`, or `defineConfig`
|
|
2697
|
+
* from `vite` over one, or either through one `const`). No ternary, no `&&`, no spread, no helper, no function.
|
|
2698
|
+
*
|
|
2699
|
+
* The precedence is Nitro's own: the options of the Vite config, then `NITRO_PRESET` / `SERVER_PRESET`, then the
|
|
2700
|
+
* Nitro config file.
|
|
2701
|
+
*/
|
|
2702
|
+
var NitroStaticConfig = class NitroStaticConfig {
|
|
2703
|
+
static nitroConfigNames = [
|
|
2704
|
+
"nitro.config.ts",
|
|
2705
|
+
"nitro.config.mts",
|
|
2706
|
+
"nitro.config.js",
|
|
2707
|
+
"nitro.config.mjs"
|
|
2708
|
+
];
|
|
2709
|
+
static viteConfigNames = [
|
|
2710
|
+
"vite.config.ts",
|
|
2711
|
+
"vite.config.mts",
|
|
2712
|
+
"vite.config.js",
|
|
2713
|
+
"vite.config.mjs"
|
|
2714
|
+
];
|
|
2715
|
+
/** Nuxt creates its own Nitro, which a static read of these files cannot describe. */
|
|
2716
|
+
static nuxtConfigNames = [
|
|
2717
|
+
"nuxt.config.ts",
|
|
2718
|
+
"nuxt.config.mts",
|
|
2719
|
+
"nuxt.config.js",
|
|
2720
|
+
"nuxt.config.mjs"
|
|
2721
|
+
];
|
|
2722
|
+
/** The extensions c12 tries for a config file, scripts and data formats. */
|
|
2723
|
+
static configExtensions = [
|
|
2724
|
+
".js",
|
|
2725
|
+
".ts",
|
|
2726
|
+
".mjs",
|
|
2727
|
+
".cjs",
|
|
2728
|
+
".mts",
|
|
2729
|
+
".cts",
|
|
2730
|
+
".json",
|
|
2731
|
+
".jsonc",
|
|
2732
|
+
".json5",
|
|
2733
|
+
".yaml",
|
|
2734
|
+
".yml",
|
|
2735
|
+
".toml"
|
|
2736
|
+
];
|
|
2737
|
+
/** The exact name of the rc file c12 reads for Nitro. */
|
|
2738
|
+
static resourceFileName = ".nitrorc";
|
|
2739
|
+
/** The expression nodes that only wrap another expression. */
|
|
2740
|
+
static wrapperTypes = [
|
|
2741
|
+
"TSAsExpression",
|
|
2742
|
+
"TSSatisfiesExpression",
|
|
2743
|
+
"TSNonNullExpression",
|
|
2744
|
+
"TSTypeAssertion",
|
|
2745
|
+
"ParenthesizedExpression"
|
|
2746
|
+
];
|
|
2747
|
+
/** The module that exports the Nitro Vite plugin. */
|
|
2748
|
+
static nitroVitePackage = "nitro/vite";
|
|
2749
|
+
/** The helper that returns its Nitro config unchanged, and where it is imported from. */
|
|
2750
|
+
static nitroConfigHelper = {
|
|
2751
|
+
name: "defineNitroConfig",
|
|
2752
|
+
packages: ["nitro", "nitro/config"]
|
|
2753
|
+
};
|
|
2754
|
+
/** The helper that returns its Vite config unchanged, and where it is imported from. */
|
|
2755
|
+
static viteConfigHelper = {
|
|
2756
|
+
name: "defineConfig",
|
|
2757
|
+
packages: ["vite", "vite-plus"]
|
|
2758
|
+
};
|
|
2759
|
+
/**
|
|
2760
|
+
* @param rootDir - The project root.
|
|
2761
|
+
* @param environment - The environment to read `NITRO_PRESET` from; the process environment by default.
|
|
2762
|
+
*/
|
|
2763
|
+
static read(rootDir, environment = process.env) {
|
|
2764
|
+
const find = (names) => names.map((name) => join(rootDir, name)).find((path) => existsSync(path));
|
|
2765
|
+
const notAHost = {
|
|
2766
|
+
isNitroHost: false,
|
|
2767
|
+
preset: { kind: "none" },
|
|
2768
|
+
serverDir: { kind: "none" },
|
|
2769
|
+
scanDirs: { kind: "none" }
|
|
2770
|
+
};
|
|
2771
|
+
if (find(NitroStaticConfig.nuxtConfigNames) !== void 0) return notAHost;
|
|
2772
|
+
const configFiles = NitroStaticConfig.configFilesOf(rootDir);
|
|
2773
|
+
const nitroFile = configFiles.length === 1 && NitroStaticConfig.nitroConfigNames.some((name) => join(rootDir, name) === configFiles[0]) ? configFiles[0] : void 0;
|
|
2774
|
+
const viteFile = find(NitroStaticConfig.viteConfigNames);
|
|
2775
|
+
const vite = viteFile === void 0 ? void 0 : NitroStaticConfig.readVite(viteFile);
|
|
2776
|
+
const hasNitroSource = configFiles.length > 0 || vite?.usesNitroPlugin === true;
|
|
2777
|
+
if (!hasNitroSource && !NitroStaticConfig.dependsOnNitro(rootDir)) return notAHost;
|
|
2778
|
+
const viteSources = vite?.sources ?? [];
|
|
2779
|
+
const fileSources = nitroFile === void 0 ? [] : NitroStaticConfig.readNitroConfig(nitroFile);
|
|
2780
|
+
const hiddenSources = NitroStaticConfig.hiddenSources(rootDir, hasNitroSource, nitroFile === void 0 ? configFiles : []);
|
|
2781
|
+
const all = [
|
|
2782
|
+
...viteSources,
|
|
2783
|
+
...fileSources,
|
|
2784
|
+
...hiddenSources
|
|
2785
|
+
];
|
|
2786
|
+
const fromVite = NitroStaticConfig.scalar("preset", viteSources, true);
|
|
2787
|
+
const fromEnvironment = environment["NITRO_PRESET"] || environment["SERVER_PRESET"];
|
|
2788
|
+
const found = fromVite.kind !== "none" ? fromVite : fromEnvironment ? {
|
|
2789
|
+
kind: "literal",
|
|
2790
|
+
value: fromEnvironment
|
|
2791
|
+
} : NitroStaticConfig.scalar("preset", [...fileSources, ...hiddenSources], true);
|
|
2792
|
+
const unreadable = all.find((source) => source.kind === "unproven");
|
|
2793
|
+
return {
|
|
2794
|
+
isNitroHost: true,
|
|
2795
|
+
preset: unreadable !== void 0 && found.kind !== "computed" ? {
|
|
2796
|
+
kind: "computed",
|
|
2797
|
+
file: unreadable.file,
|
|
2798
|
+
reason: unreadable.reason
|
|
2799
|
+
} : NitroStaticConfig.withHidden(found, all, ["static", "extends"], ["defaultPreset"]),
|
|
2800
|
+
serverDir: NitroStaticConfig.withHidden(NitroStaticConfig.scalar("serverDir", all), all, [], ["extends"]),
|
|
2801
|
+
scanDirs: NitroStaticConfig.withHiddenList(NitroStaticConfig.list("scanDirs", all), all)
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Sources of options that the project has but the reader cannot read: a `.nitrorc` file (in the project or the
|
|
2806
|
+
* home directory), the `nitro` key of `package.json`, and a dependency on `nitro` when no config file or
|
|
2807
|
+
* provable plugin call exists.
|
|
2808
|
+
*/
|
|
2809
|
+
static hiddenSources(rootDir, hasNitroSource, unreadFiles) {
|
|
2810
|
+
const unproven = (reason) => ({
|
|
2811
|
+
kind: "unproven",
|
|
2812
|
+
file: join(rootDir, "package.json"),
|
|
2813
|
+
reason
|
|
2814
|
+
});
|
|
2815
|
+
const sources = [];
|
|
2816
|
+
const resourceFile = [rootDir, homedir()].map((directory) => join(directory, NitroStaticConfig.resourceFileName)).find((path) => existsSync(path));
|
|
2817
|
+
if (resourceFile !== void 0) sources.push(unproven(`the file ${resourceFile} sets Nitro options`));
|
|
2818
|
+
if (unreadFiles.length > 0) sources.push(unproven(`the file ${unreadFiles[0]} sets Nitro options in a form the command does not read`));
|
|
2819
|
+
const manifest = NitroStaticConfig.manifestOf(rootDir);
|
|
2820
|
+
if (manifest === "invalid") sources.push(unproven("package.json is not valid JSON"));
|
|
2821
|
+
else if (manifest !== void 0) {
|
|
2822
|
+
if (manifest.nitro !== void 0) sources.push(unproven("the `nitro` key of package.json sets Nitro options"));
|
|
2823
|
+
if (!hasNitroSource && NitroStaticConfig.dependsOnNitro(rootDir)) sources.push(unproven("the project depends on nitro, but no Nitro config file or Nitro Vite plugin call was found"));
|
|
2824
|
+
}
|
|
2825
|
+
return sources;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Every file or directory that the config loader of Nitro (c12) would take as its main config: the name
|
|
2829
|
+
* `nitro.config` in the project, in `.config/`, and `.config/nitro`, bare, with each supported extension, and as
|
|
2830
|
+
* a directory with an `index` file. The project's own script forms are among them.
|
|
2831
|
+
*/
|
|
2832
|
+
static configFilesOf(rootDir) {
|
|
2833
|
+
return [
|
|
2834
|
+
join(rootDir, "nitro.config"),
|
|
2835
|
+
join(rootDir, ".config", "nitro.config"),
|
|
2836
|
+
join(rootDir, ".config", "nitro")
|
|
2837
|
+
].flatMap((base) => ["", "/index"].flatMap((suffix) => ["", ...NitroStaticConfig.configExtensions].map((extension) => `${base}${suffix}${extension}`))).filter((path) => existsSync(path));
|
|
2838
|
+
}
|
|
2839
|
+
/** The parsed package.json of the project, `undefined` when it has none, `"invalid"` when it is not JSON. */
|
|
2840
|
+
static manifestOf(rootDir) {
|
|
2841
|
+
const file = join(rootDir, "package.json");
|
|
2842
|
+
if (!existsSync(file)) return;
|
|
2843
|
+
try {
|
|
2844
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
2845
|
+
} catch {
|
|
2846
|
+
return "invalid";
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
/** Whether package.json lists `nitro` as a dependency or a dev dependency. */
|
|
2850
|
+
static dependsOnNitro(rootDir) {
|
|
2851
|
+
const manifest = NitroStaticConfig.manifestOf(rootDir);
|
|
2852
|
+
return manifest !== void 0 && manifest !== "invalid" && ("nitro" in (manifest.dependencies ?? {}) || "nitro" in (manifest.devDependencies ?? {}));
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Apply the keys that change a value without showing in it. A key in `always` (for the preset: `static`, which
|
|
2856
|
+
* turns a preset into its static variant) makes the value unknown. A key in `whenUnset` (`defaultPreset`,
|
|
2857
|
+
* `extends`: the layers sit below every other source) matters only when no literal was found.
|
|
2858
|
+
*/
|
|
2859
|
+
static withHidden(value, sources, always, whenUnset) {
|
|
2860
|
+
const hidden = (keys) => {
|
|
2861
|
+
for (const source of sources) {
|
|
2862
|
+
const key = source.kind === "object" ? keys.find((name) => source.entries.has(name)) : void 0;
|
|
2863
|
+
if (source.kind === "object" && key !== void 0) return {
|
|
2864
|
+
file: source.file,
|
|
2865
|
+
key
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2869
|
+
const alwaysHidden = hidden(always);
|
|
2870
|
+
const unsetHidden = value.kind === "none" ? hidden(whenUnset) : void 0;
|
|
2871
|
+
const reason = alwaysHidden ?? unsetHidden;
|
|
2872
|
+
return reason === void 0 ? value : {
|
|
2873
|
+
kind: "computed",
|
|
2874
|
+
file: reason.file,
|
|
2875
|
+
reason: `\`${reason.key}\` can change the value`
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
/** A list gets every `extends` layer on top of it, so a layer makes it unknown. */
|
|
2879
|
+
static withHiddenList(value, sources) {
|
|
2880
|
+
const layered = sources.find((source) => source.kind === "object" && source.entries.has("extends"));
|
|
2881
|
+
return layered === void 0 || layered.kind !== "object" ? value : {
|
|
2882
|
+
kind: "computed",
|
|
2883
|
+
file: layered.file,
|
|
2884
|
+
reason: "`extends` can add values"
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* The option sources of a Nitro config file, highest priority first: the production overlays, then the
|
|
2889
|
+
* object itself. An `extends` or `defaultPreset` key brings values that the source does not show, so it adds
|
|
2890
|
+
* an unproven source below the object.
|
|
2891
|
+
*/
|
|
2892
|
+
static readNitroConfig(file) {
|
|
2893
|
+
const { program, imports } = NitroStaticConfig.parse(file);
|
|
2894
|
+
const resolved = NitroStaticConfig.exportedObject(program, imports, NitroStaticConfig.nitroConfigHelper);
|
|
2895
|
+
if (resolved.kind === "unproven") return [{
|
|
2896
|
+
kind: "unproven",
|
|
2897
|
+
file,
|
|
2898
|
+
reason: resolved.reason
|
|
2899
|
+
}];
|
|
2900
|
+
const base = NitroStaticConfig.prove(resolved.node, file);
|
|
2901
|
+
if (base.kind === "unproven") return [base];
|
|
2902
|
+
const overlay = /* @__PURE__ */ new Map();
|
|
2903
|
+
const sources = [];
|
|
2904
|
+
const overlays = [base.entries.get("$production")];
|
|
2905
|
+
const environments = base.entries.get("$env");
|
|
2906
|
+
if (environments !== void 0) {
|
|
2907
|
+
const proven = NitroStaticConfig.prove(environments, file);
|
|
2908
|
+
if (proven.kind === "unproven") sources.push(proven);
|
|
2909
|
+
else overlays.push(proven.entries.get("production"));
|
|
2910
|
+
}
|
|
2911
|
+
for (const node of overlays) {
|
|
2912
|
+
if (node === void 0) continue;
|
|
2913
|
+
const proven = NitroStaticConfig.prove(node, file);
|
|
2914
|
+
if (proven.kind === "unproven") sources.push(proven);
|
|
2915
|
+
else for (const [key, value] of proven.entries) overlay.set(key, value);
|
|
2916
|
+
}
|
|
2917
|
+
if (overlay.size > 0) sources.push({
|
|
2918
|
+
kind: "object",
|
|
2919
|
+
file,
|
|
2920
|
+
entries: overlay
|
|
2921
|
+
});
|
|
2922
|
+
sources.push(base);
|
|
2923
|
+
return sources;
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* What the Vite config says: whether it imports the Nitro plugin, and the option sources it gives. Once the
|
|
2927
|
+
* plugin is imported, anything the reader cannot prove makes the sources unproven; the project is still a
|
|
2928
|
+
* Nitro host.
|
|
2929
|
+
*/
|
|
2930
|
+
static readVite(file) {
|
|
2931
|
+
const { program, imports } = NitroStaticConfig.parse(file);
|
|
2932
|
+
const named = /* @__PURE__ */ new Set();
|
|
2933
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
2934
|
+
for (const entry of imports) {
|
|
2935
|
+
if (entry.request !== NitroStaticConfig.nitroVitePackage || entry.isTypeOnly) continue;
|
|
2936
|
+
if (entry.importKind === ImportKind.Namespace) namespaces.add(entry.localName);
|
|
2937
|
+
else if (entry.importKind === ImportKind.Named && entry.importedName === "nitro") named.add(entry.localName);
|
|
2938
|
+
}
|
|
2939
|
+
if (named.size === 0 && namespaces.size === 0) return {
|
|
2940
|
+
usesNitroPlugin: false,
|
|
2941
|
+
sources: []
|
|
2942
|
+
};
|
|
2943
|
+
const unproven = (reason) => ({
|
|
2944
|
+
usesNitroPlugin: true,
|
|
2945
|
+
sources: [{
|
|
2946
|
+
kind: "unproven",
|
|
2947
|
+
file,
|
|
2948
|
+
reason
|
|
2949
|
+
}]
|
|
2950
|
+
});
|
|
2951
|
+
const importStarts = new Set(program.descendants("ImportDeclaration").flatMap((declaration) => declaration.descendants(AstNodeType.Identifier)).map((identifier) => identifier.start));
|
|
2952
|
+
const calls = program.descendants(AstNodeType.CallExpression).flatMap((call) => {
|
|
2953
|
+
const callee = call.child("callee");
|
|
2954
|
+
if (callee.is(AstNodeType.Identifier) && named.has(callee.name ?? "")) return [{
|
|
2955
|
+
call,
|
|
2956
|
+
referenceStart: callee.start
|
|
2957
|
+
}];
|
|
2958
|
+
if (callee.is(AstNodeType.MemberExpression) && !callee.flagAt("computed") && callee.child("property").name === "nitro" && callee.child("object").is(AstNodeType.Identifier) && namespaces.has(callee.child("object").name ?? "")) return [{
|
|
2959
|
+
call,
|
|
2960
|
+
referenceStart: callee.child("object").start
|
|
2961
|
+
}];
|
|
2962
|
+
return [];
|
|
2963
|
+
});
|
|
2964
|
+
const callStarts = new Set(calls.map((entry) => entry.referenceStart));
|
|
2965
|
+
const nameStarts = NitroStaticConfig.nameStarts(program);
|
|
2966
|
+
if (program.descendants(AstNodeType.Identifier).filter((identifier) => (named.has(identifier.name ?? "") || namespaces.has(identifier.name ?? "")) && !importStarts.has(identifier.start) && !nameStarts.has(identifier.start) && !callStarts.has(identifier.start)).length > 0) return unproven("the Nitro Vite plugin is used other than by a direct call");
|
|
2967
|
+
if (calls.length !== 1) return unproven(`the Nitro Vite plugin is called ${calls.length} times, not once`);
|
|
2968
|
+
const resolved = NitroStaticConfig.exportedObject(program, imports, NitroStaticConfig.viteConfigHelper);
|
|
2969
|
+
if (resolved.kind === "unproven") return unproven(resolved.reason);
|
|
2970
|
+
const config = NitroStaticConfig.prove(resolved.node, file);
|
|
2971
|
+
if (config.kind === "unproven") return {
|
|
2972
|
+
usesNitroPlugin: true,
|
|
2973
|
+
sources: [config]
|
|
2974
|
+
};
|
|
2975
|
+
const plugins = NitroStaticConfig.unwrap(config.entries.get("plugins"));
|
|
2976
|
+
const call = calls[0].call;
|
|
2977
|
+
if (!(plugins?.is(AstNodeType.ArrayExpression) === true && plugins.children("elements").some((element) => NitroStaticConfig.unwrap(element)?.start === call.start))) return unproven("the Nitro Vite plugin is not a direct element of the `plugins` array");
|
|
2978
|
+
const sources = [];
|
|
2979
|
+
const argument = NitroStaticConfig.unwrap(call.children("arguments")[0]);
|
|
2980
|
+
if (argument !== void 0) {
|
|
2981
|
+
const options = NitroStaticConfig.prove(argument, file);
|
|
2982
|
+
sources.push(options);
|
|
2983
|
+
const nested = options.kind === "object" ? options.entries.get("config") : void 0;
|
|
2984
|
+
if (nested !== void 0) sources.push(NitroStaticConfig.prove(nested, file));
|
|
2985
|
+
}
|
|
2986
|
+
const nitroKey = config.entries.get("nitro");
|
|
2987
|
+
if (nitroKey !== void 0) sources.push(NitroStaticConfig.prove(nitroKey, file));
|
|
2988
|
+
return {
|
|
2989
|
+
usesNitroPlugin: true,
|
|
2990
|
+
sources
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* The object that `export default` gives: a literal, a call of the verified identity helper over one, or a
|
|
2995
|
+
* single top-level `const` that holds either and is never assigned again.
|
|
2996
|
+
*/
|
|
2997
|
+
static exportedObject(program, imports, helper) {
|
|
2998
|
+
const helperNames = new Set(imports.filter((entry) => entry.importKind === ImportKind.Named && entry.importedName === helper.name && helper.packages.includes(entry.request) && !entry.isTypeOnly).map((entry) => entry.localName));
|
|
2999
|
+
const declaration = program.children("body").find((statement) => statement.is(AstNodeType.ExportDefaultDeclaration))?.child("declaration");
|
|
3000
|
+
const objectOf = (node, viaConstant) => {
|
|
3001
|
+
const expression = NitroStaticConfig.unwrap(node);
|
|
3002
|
+
if (expression === void 0) return {
|
|
3003
|
+
kind: "unproven",
|
|
3004
|
+
reason: "the config has no default export"
|
|
3005
|
+
};
|
|
3006
|
+
if (expression.is(AstNodeType.ObjectExpression)) return {
|
|
3007
|
+
kind: "found",
|
|
3008
|
+
node: expression
|
|
3009
|
+
};
|
|
3010
|
+
if (expression.is(AstNodeType.CallExpression)) {
|
|
3011
|
+
const callee = expression.child("callee");
|
|
3012
|
+
return callee.is(AstNodeType.Identifier) && helperNames.has(callee.name ?? "") && expression.children("arguments").length === 1 ? objectOf(expression.children("arguments")[0], viaConstant) : {
|
|
3013
|
+
kind: "unproven",
|
|
3014
|
+
reason: `the config is a call of something other than ${helper.name} from ${helper.packages.join(" or ")}`
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
if (expression.is(AstNodeType.Identifier) && !viaConstant) {
|
|
3018
|
+
const initializer = NitroStaticConfig.constantOf(program, expression.name ?? "", expression.start);
|
|
3019
|
+
return initializer === void 0 ? {
|
|
3020
|
+
kind: "unproven",
|
|
3021
|
+
reason: `\`${expression.name}\` is not a top-level const that nothing else uses`
|
|
3022
|
+
} : objectOf(initializer, true);
|
|
3023
|
+
}
|
|
3024
|
+
return {
|
|
3025
|
+
kind: "unproven",
|
|
3026
|
+
reason: "the default export is not an object the reader can prove"
|
|
3027
|
+
};
|
|
3028
|
+
};
|
|
3029
|
+
return objectOf(declaration, false);
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* The initializer of `const <name> = …` at the top level, when the name has no use except the one at
|
|
3033
|
+
* `exportStart` (the default export, or the single wrapper call around it). A member assignment
|
|
3034
|
+
* (`config.preset = …`), a call that gets the name (`Object.assign(config, …)`), or a spread of it is another
|
|
3035
|
+
* use, and it makes the value unknown.
|
|
3036
|
+
*/
|
|
3037
|
+
static constantOf(program, name, exportStart) {
|
|
3038
|
+
const initializers = [];
|
|
3039
|
+
for (const statement of program.children("body")) {
|
|
3040
|
+
const declaration = statement.is(AstNodeType.ExportNamedDeclaration) ? statement.child("declaration") : statement;
|
|
3041
|
+
if (declaration.stringAt("kind") !== "const") continue;
|
|
3042
|
+
for (const declarator of declaration.children("declarations")) if (declarator.child("id").name === name) initializers.push({
|
|
3043
|
+
init: declarator.child("init"),
|
|
3044
|
+
idStart: declarator.child("id").start
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
if (initializers.length !== 1) return;
|
|
3048
|
+
const names = NitroStaticConfig.nameStarts(program);
|
|
3049
|
+
return program.descendants(AstNodeType.Identifier).filter((identifier) => identifier.name === name && identifier.start !== initializers[0].idStart && identifier.start !== exportStart && !names.has(identifier.start)).length === 0 ? initializers[0].init : void 0;
|
|
3050
|
+
}
|
|
3051
|
+
/** The starts of identifiers that are names, not references: a property key and a member name. */
|
|
3052
|
+
static nameStarts(program) {
|
|
3053
|
+
return /* @__PURE__ */ new Set([...program.descendants(AstNodeType.Property).filter((property) => !property.flagAt("computed") && !property.flagAt("shorthand")).map((property) => property.child("key").start), ...program.descendants(AstNodeType.MemberExpression).filter((member) => !member.flagAt("computed")).map((member) => member.child("property").start)]);
|
|
3054
|
+
}
|
|
3055
|
+
/** The options of one object, or the reason that the reader cannot prove it. */
|
|
3056
|
+
static prove(node, file) {
|
|
3057
|
+
const object = NitroStaticConfig.unwrap(node);
|
|
3058
|
+
if (object === void 0 || !object.is(AstNodeType.ObjectExpression)) return {
|
|
3059
|
+
kind: "unproven",
|
|
3060
|
+
file,
|
|
3061
|
+
reason: "an options value is not an object literal"
|
|
3062
|
+
};
|
|
3063
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3064
|
+
for (const property of object.children("properties")) {
|
|
3065
|
+
if (!property.is(AstNodeType.Property)) return {
|
|
3066
|
+
kind: "unproven",
|
|
3067
|
+
file,
|
|
3068
|
+
reason: "an options object has a spread"
|
|
3069
|
+
};
|
|
3070
|
+
const key = property.child("key");
|
|
3071
|
+
const name = key.name ?? key.stringAt("value");
|
|
3072
|
+
if (property.flagAt("computed") || property.flagAt("method") || property.stringAt("kind") !== "init" || name === void 0) return {
|
|
3073
|
+
kind: "unproven",
|
|
3074
|
+
file,
|
|
3075
|
+
reason: "an options object has a computed key, a getter or a method"
|
|
3076
|
+
};
|
|
3077
|
+
entries.set(name, property.child("value"));
|
|
3078
|
+
}
|
|
3079
|
+
return {
|
|
3080
|
+
kind: "object",
|
|
3081
|
+
file,
|
|
3082
|
+
entries
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
/** A string option: the first source that sets it decides. */
|
|
3086
|
+
static scalar(key, sources, emptyIsAbsent = false) {
|
|
3087
|
+
for (const source of sources) {
|
|
3088
|
+
if (source.kind === "unproven") return {
|
|
3089
|
+
kind: "computed",
|
|
3090
|
+
file: source.file,
|
|
3091
|
+
reason: source.reason
|
|
3092
|
+
};
|
|
3093
|
+
const node = source.entries.get(key);
|
|
3094
|
+
if (node === void 0) continue;
|
|
3095
|
+
const value = NitroStaticConfig.stringOf(node);
|
|
3096
|
+
if (emptyIsAbsent && value === "") continue;
|
|
3097
|
+
return value === void 0 ? {
|
|
3098
|
+
kind: "computed",
|
|
3099
|
+
file: source.file,
|
|
3100
|
+
reason: `\`${key}\` is not a string literal`
|
|
3101
|
+
} : {
|
|
3102
|
+
kind: "literal",
|
|
3103
|
+
value
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
return { kind: "none" };
|
|
3107
|
+
}
|
|
3108
|
+
/** A list option: every source adds to it, so one unproven source makes it unknown. */
|
|
3109
|
+
static list(key, sources) {
|
|
3110
|
+
const values = [];
|
|
3111
|
+
let found = false;
|
|
3112
|
+
for (const source of sources) {
|
|
3113
|
+
if (source.kind === "unproven") return {
|
|
3114
|
+
kind: "computed",
|
|
3115
|
+
file: source.file,
|
|
3116
|
+
reason: source.reason
|
|
3117
|
+
};
|
|
3118
|
+
const node = NitroStaticConfig.unwrap(source.entries.get(key));
|
|
3119
|
+
if (node === void 0) continue;
|
|
3120
|
+
const strings = (node.is(AstNodeType.ArrayExpression) ? node.children("elements") : []).map((element) => NitroStaticConfig.stringOf(element));
|
|
3121
|
+
if (!node.is(AstNodeType.ArrayExpression) || strings.some((value) => value === void 0)) return {
|
|
3122
|
+
kind: "computed",
|
|
3123
|
+
file: source.file,
|
|
3124
|
+
reason: `\`${key}\` is not an array of string literals`
|
|
3125
|
+
};
|
|
3126
|
+
found = true;
|
|
3127
|
+
values.push(...strings);
|
|
3128
|
+
}
|
|
3129
|
+
return found ? {
|
|
3130
|
+
kind: "literal",
|
|
3131
|
+
value: values
|
|
3132
|
+
} : { kind: "none" };
|
|
3133
|
+
}
|
|
3134
|
+
/** The text of a string literal or of a template literal with no placeholder; else `undefined`. */
|
|
3135
|
+
static stringOf(node) {
|
|
3136
|
+
const value = NitroStaticConfig.unwrap(node);
|
|
3137
|
+
if (value === void 0) return;
|
|
3138
|
+
if (typeof value.literalValue === "string") return value.literalValue;
|
|
3139
|
+
if (value.is(AstNodeType.TemplateLiteral) && !value.hasChildren("expressions")) return value.children("quasis")[0]?.child("value").stringAt("cooked");
|
|
3140
|
+
}
|
|
3141
|
+
/** The node without its wrappers (`as`, `satisfies`, `!`, parentheses); `undefined` when absent. */
|
|
3142
|
+
static unwrap(node) {
|
|
3143
|
+
let current = node;
|
|
3144
|
+
while (current !== void 0 && current.exists() && NitroStaticConfig.wrapperTypes.includes(current.type ?? "")) current = current.child("expression");
|
|
3145
|
+
return current === void 0 || !current.exists() ? void 0 : current;
|
|
3146
|
+
}
|
|
3147
|
+
static parse(file) {
|
|
3148
|
+
const parsed = new SourceParser().parse(file, readFileSync(file, "utf8"));
|
|
3149
|
+
return {
|
|
3150
|
+
program: parsed.program,
|
|
3151
|
+
imports: parsed.imports
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
};
|
|
3155
|
+
//#endregion
|
|
3156
|
+
//#region src/runtime-export-resolver.ts
|
|
3157
|
+
/**
|
|
3158
|
+
* Tells whether a name that generated code imports is a given export of a package, through the barrel files
|
|
3159
|
+
* of the app. Generated code imports from app source (`../../src/runtime-barrel.js`), and a barrel can
|
|
3160
|
+
* re-export the package under the same name, another name, or through `export *`. The chain is followed with
|
|
3161
|
+
* the compiler's own {@link ReexportIndex}.
|
|
3162
|
+
*/
|
|
3163
|
+
var RuntimeExportResolver = class RuntimeExportResolver {
|
|
3164
|
+
outputDirectory;
|
|
3165
|
+
packageName;
|
|
3166
|
+
exportName;
|
|
3167
|
+
parser = new SourceParser();
|
|
3168
|
+
/** The parsed barrel files reached so far, by absolute path. */
|
|
3169
|
+
files = /* @__PURE__ */ new Map();
|
|
3170
|
+
/** The index over {@link files}; dropped when a file joins. */
|
|
3171
|
+
index;
|
|
3172
|
+
/**
|
|
3173
|
+
* @param outputDirectory - The compile output directory, the base of the relative requests of generated code.
|
|
3174
|
+
* @param packageName - The package whose export is looked for (`@heximon/runtime`).
|
|
3175
|
+
* @param exportName - The export looked for (`Platform`).
|
|
3176
|
+
*/
|
|
3177
|
+
constructor(outputDirectory, packageName, exportName) {
|
|
3178
|
+
this.outputDirectory = outputDirectory;
|
|
3179
|
+
this.packageName = packageName;
|
|
3180
|
+
this.exportName = exportName;
|
|
3181
|
+
}
|
|
3182
|
+
/**
|
|
3183
|
+
* Whether `name`, imported from `request` by the generated file `importer`, is the export looked for.
|
|
3184
|
+
*
|
|
3185
|
+
* @param importer - The generated file's path, relative to the output directory.
|
|
3186
|
+
* @param request - The module request of the import.
|
|
3187
|
+
* @param name - The imported name; `exportName` for a namespace member.
|
|
3188
|
+
*/
|
|
3189
|
+
isTarget(importer, request, name) {
|
|
3190
|
+
if (request === this.packageName) return name === this.exportName;
|
|
3191
|
+
if (!request.startsWith(".")) return false;
|
|
3192
|
+
const barrel = RuntimeExportResolver.resolveFile(dirname(join(this.outputDirectory, importer)), request);
|
|
3193
|
+
if (barrel === void 0) return false;
|
|
3194
|
+
this.load(barrel);
|
|
3195
|
+
const canonical = this.indexOfFiles().canonicalize({
|
|
3196
|
+
sourceFile: barrel,
|
|
3197
|
+
className: name
|
|
3198
|
+
});
|
|
3199
|
+
return canonical.sourceFile === this.packageName && canonical.className === this.exportName;
|
|
3200
|
+
}
|
|
3201
|
+
/** The source file a relative request names, or `undefined` when no file exists (a generated file, for one). */
|
|
3202
|
+
static resolveFile(directory, request) {
|
|
3203
|
+
return SourceModuleResolution.resolve(join(directory, request));
|
|
3204
|
+
}
|
|
3205
|
+
/** Parse `path` and the files its export statements lead to. */
|
|
3206
|
+
load(path) {
|
|
3207
|
+
if (this.files.has(path)) return;
|
|
3208
|
+
let parsed;
|
|
3209
|
+
try {
|
|
3210
|
+
parsed = this.parser.parse(path, readFileSync(path, "utf8"));
|
|
3211
|
+
} catch {
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
this.files.set(path, parsed);
|
|
3215
|
+
this.index = void 0;
|
|
3216
|
+
const exportedLocals = new Set(parsed.exports.flatMap((entry) => entry.request === null ? [entry.localName ?? entry.exportedName] : []));
|
|
3217
|
+
const requests = [...parsed.exports.map((entry) => entry.request), ...parsed.imports.filter((entry) => exportedLocals.has(entry.localName)).map((entry) => entry.request)];
|
|
3218
|
+
for (const request of requests) if (request?.startsWith(".")) {
|
|
3219
|
+
const next = RuntimeExportResolver.resolveFile(dirname(path), request);
|
|
3220
|
+
if (next !== void 0) this.load(next);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
indexOfFiles() {
|
|
3224
|
+
this.index ??= new ReexportIndex([...this.files].map(([filePath, parsed]) => ({
|
|
3225
|
+
filePath,
|
|
3226
|
+
exports: parsed.exports,
|
|
3227
|
+
imports: parsed.imports
|
|
3228
|
+
})), (from, request) => RuntimeExportResolver.resolveFile(dirname(from), request) ?? null, false, (specifier, name) => specifier === this.packageName && name === this.exportName);
|
|
3229
|
+
return this.index;
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
3232
|
+
//#endregion
|
|
3233
|
+
//#region src/platform-binding-census.ts
|
|
3234
|
+
/**
|
|
3235
|
+
* Finds the platform bindings (Workers KV, Hyperdrive, Durable Objects, …) a compiled graph needs, before
|
|
3236
|
+
* anything boots. A binding exists only on its platform, so a run outside it must refuse instead of failing
|
|
3237
|
+
* on the first read.
|
|
3238
|
+
*
|
|
3239
|
+
* Two facts name a binding. The generated wiring holds the string literal of each `Platform.binding("NAME")`
|
|
3240
|
+
* call, with the module that owns the file. The deploy-config fragments of the compile list the bindings the
|
|
3241
|
+
* sidecar declares. A binding read through a variable name is in neither place, and the run then fails on
|
|
3242
|
+
* the read itself.
|
|
3243
|
+
*/
|
|
3244
|
+
var PlatformBindingCensus = class PlatformBindingCensus {
|
|
3245
|
+
/** The package whose `Platform` the generated code reads bindings from. */
|
|
3246
|
+
static runtimePackage = "@heximon/runtime";
|
|
3247
|
+
/** The `Platform` class and the method of it that reads a binding. */
|
|
3248
|
+
static platformExport = "Platform";
|
|
3249
|
+
static bindingMethod = "binding";
|
|
3250
|
+
/** The module wiring files: `modules/<name>.wiring.js` exports `init<ModuleClass>`. */
|
|
3251
|
+
static moduleInitializerPattern = /export async function init(\w+)\(/;
|
|
3252
|
+
static platformBindingKind = "platform binding";
|
|
3253
|
+
/** The deploy-config sections that declare bindings, by section name. */
|
|
3254
|
+
static declaringSections = {
|
|
3255
|
+
"kv-namespaces": (payload) => PlatformBindingCensus.entriesOf(payload["kvNamespaces"], "binding").map((binding) => ({
|
|
3256
|
+
binding,
|
|
3257
|
+
neededBy: "`cloudflare.kvNamespaces` in heximon.config.ts"
|
|
3258
|
+
})),
|
|
3259
|
+
hyperdrive: (payload) => PlatformBindingCensus.entriesOf(payload["hyperdrive"], "binding").map((binding) => ({
|
|
3260
|
+
binding,
|
|
3261
|
+
neededBy: "`cloudflare.hyperdrive` in heximon.config.ts"
|
|
3262
|
+
})),
|
|
3263
|
+
"r2-buckets": (payload) => PlatformBindingCensus.entriesOf(payload["r2Buckets"], "binding").map((binding) => ({
|
|
3264
|
+
binding,
|
|
3265
|
+
neededBy: "`cloudflare.r2Buckets` in heximon.config.ts"
|
|
3266
|
+
})),
|
|
3267
|
+
browser: (payload) => PlatformBindingCensus.entriesOf([payload["browser"]], "binding").map((binding) => ({
|
|
3268
|
+
binding,
|
|
3269
|
+
neededBy: "`cloudflare.browser` in heximon.config.ts"
|
|
3270
|
+
})),
|
|
3271
|
+
"queue-bindings": (payload) => PlatformBindingCensus.entriesOf(payload["producers"], "binding").map((binding) => ({
|
|
3272
|
+
binding,
|
|
3273
|
+
neededBy: "`cloudflare.queues` in heximon.config.ts"
|
|
3274
|
+
})),
|
|
3275
|
+
"integration-event-subscriptions": (payload) => PlatformBindingCensus.entriesOf(payload["producers"], "binding").map((binding) => ({
|
|
3276
|
+
binding,
|
|
3277
|
+
neededBy: "the cross-service integration events"
|
|
3278
|
+
})),
|
|
3279
|
+
"workflow-bindings": (payload) => PlatformBindingCensus.entriesOf(payload["workflows"], "binding").map((binding) => ({
|
|
3280
|
+
binding,
|
|
3281
|
+
neededBy: "a Cloudflare workflow"
|
|
3282
|
+
})),
|
|
3283
|
+
"durable-objects": (payload) => PlatformBindingCensus.durableBindings(payload["bindings"])
|
|
3284
|
+
};
|
|
3285
|
+
/** The kind label of each deploy-config section. */
|
|
3286
|
+
static sectionKinds = {
|
|
3287
|
+
"kv-namespaces": "Workers KV",
|
|
3288
|
+
hyperdrive: "Hyperdrive",
|
|
3289
|
+
"r2-buckets": "R2 bucket",
|
|
3290
|
+
browser: "Browser Rendering",
|
|
3291
|
+
"queue-bindings": "Cloudflare Queue",
|
|
3292
|
+
"integration-event-subscriptions": "Cloudflare Queue",
|
|
3293
|
+
"workflow-bindings": "Cloudflare Workflow",
|
|
3294
|
+
"durable-objects": "Durable Object"
|
|
3295
|
+
};
|
|
3296
|
+
/**
|
|
3297
|
+
* The bindings the compile output needs, sorted by name.
|
|
3298
|
+
*
|
|
3299
|
+
* @param output - The production compile output.
|
|
3300
|
+
* @param platformName - The platform the graph was compiled for. A deploy-config fragment for another
|
|
3301
|
+
* platform is inert and does not count: the Durable Objects of a Node graph run in-process.
|
|
3302
|
+
* @param outputDirectory - The compile output directory, where the relative imports of generated code start.
|
|
3303
|
+
* @returns One entry for each binding name, with every reason the compile found.
|
|
3304
|
+
*/
|
|
3305
|
+
static collect(output, platformName, outputDirectory) {
|
|
3306
|
+
const needs = /* @__PURE__ */ new Map();
|
|
3307
|
+
const record = (binding, kind, neededBy) => {
|
|
3308
|
+
const known = needs.get(binding);
|
|
3309
|
+
if (known === void 0) {
|
|
3310
|
+
needs.set(binding, {
|
|
3311
|
+
kind,
|
|
3312
|
+
neededBy: /* @__PURE__ */ new Set([neededBy])
|
|
3313
|
+
});
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
known.neededBy.add(neededBy);
|
|
3317
|
+
if (known.kind === PlatformBindingCensus.platformBindingKind) known.kind = kind;
|
|
3318
|
+
};
|
|
3319
|
+
const hostEntryPaths = new Set(output.hostEntries.map((entry) => entry.path));
|
|
3320
|
+
const platform = new RuntimeExportResolver(outputDirectory, PlatformBindingCensus.runtimePackage, PlatformBindingCensus.platformExport);
|
|
3321
|
+
for (const file of output.files) {
|
|
3322
|
+
if (!file.path.endsWith(".js") || hostEntryPaths.has(file.path)) continue;
|
|
3323
|
+
for (const binding of PlatformBindingCensus.calledBindings(file, platform)) record(binding, PlatformBindingCensus.platformBindingKind, PlatformBindingCensus.ownerOf(file));
|
|
3324
|
+
}
|
|
3325
|
+
for (const contribution of output.buildConfigContributions) for (const declared of PlatformBindingCensus.declaredBy(contribution, platformName)) record(declared.binding, PlatformBindingCensus.sectionKinds[contribution.section] ?? PlatformBindingCensus.platformBindingKind, declared.neededBy);
|
|
3326
|
+
return [...needs.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([binding, need]) => ({
|
|
3327
|
+
binding,
|
|
3328
|
+
kind: need.kind,
|
|
3329
|
+
neededBy: [...need.neededBy].sort()
|
|
3330
|
+
}));
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* The refusal text: each binding with its kind and the places that need it.
|
|
3334
|
+
*
|
|
3335
|
+
* @param needs - The bindings from {@link collect}.
|
|
3336
|
+
* @param platformName - The platform of the compiled graph, when the sidecar names one.
|
|
3337
|
+
*/
|
|
3338
|
+
static explain(needs, platformName) {
|
|
3339
|
+
const lines = needs.map((need) => ` - ${need.binding} (${need.kind}): ${need.neededBy.join("; ")}`);
|
|
3340
|
+
return [
|
|
3341
|
+
`This graph needs platform bindings that exist only on ${platformName ?? "its deploy platform"}, so a run outside it would fail on the first read. Nothing was booted.`,
|
|
3342
|
+
...lines,
|
|
3343
|
+
"Run this task on the platform that has these bindings."
|
|
3344
|
+
].join("\n");
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* The names in the literal `Platform.binding("NAME")` calls of one generated file. The callee must resolve
|
|
3348
|
+
* to the `Platform` of `@heximon/runtime`, under any local name, through a namespace import, or through the
|
|
3349
|
+
* barrel files of the app that re-export it; a call on another object is not a binding read.
|
|
3350
|
+
*/
|
|
3351
|
+
static calledBindings(file, platform) {
|
|
3352
|
+
let parsed;
|
|
3353
|
+
try {
|
|
3354
|
+
parsed = new SourceParser().parse(file.path, file.content);
|
|
3355
|
+
} catch {
|
|
3356
|
+
return [];
|
|
3357
|
+
}
|
|
3358
|
+
if (parsed.errors.length > 0) return [];
|
|
3359
|
+
const platformNames = /* @__PURE__ */ new Set();
|
|
3360
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
3361
|
+
for (const entry of parsed.imports) {
|
|
3362
|
+
if (entry.isTypeOnly) continue;
|
|
3363
|
+
if (entry.importKind === ImportKind.Namespace) namespaces.set(entry.localName, entry.request);
|
|
3364
|
+
else if (entry.importKind === ImportKind.Named && platform.isTarget(file.path, entry.request, entry.importedName)) platformNames.add(entry.localName);
|
|
3365
|
+
}
|
|
3366
|
+
const bindings = [];
|
|
3367
|
+
for (const call of parsed.program.descendants(AstNodeType.CallExpression)) {
|
|
3368
|
+
const callee = call.child("callee");
|
|
3369
|
+
if (callee.is(AstNodeType.MemberExpression) && PlatformBindingCensus.memberName(callee) === PlatformBindingCensus.bindingMethod && PlatformBindingCensus.isPlatform(callee.child("object"), file.path, platform, platformNames, namespaces)) {
|
|
3370
|
+
const name = PlatformBindingCensus.literalText(call.children("arguments")[0]);
|
|
3371
|
+
if (name !== void 0) bindings.push(name);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
return bindings;
|
|
3375
|
+
}
|
|
3376
|
+
/** Whether `node` names the runtime `Platform`: an imported name, or `<namespace>.<name>` of a module that exports it. */
|
|
3377
|
+
static isPlatform(node, importer, platform, platformNames, namespaces) {
|
|
3378
|
+
if (node.is(AstNodeType.Identifier)) return platformNames.has(node.name ?? "");
|
|
3379
|
+
const namespace = node.child("object");
|
|
3380
|
+
const request = namespace.is(AstNodeType.Identifier) ? namespaces.get(namespace.name ?? "") : void 0;
|
|
3381
|
+
const member = node.is(AstNodeType.MemberExpression) ? PlatformBindingCensus.memberName(node) : void 0;
|
|
3382
|
+
return request !== void 0 && member !== void 0 && platform.isTarget(importer, request, member);
|
|
3383
|
+
}
|
|
3384
|
+
/** The property name of `object.name` or `object["name"]`, or `undefined` for a computed name. */
|
|
3385
|
+
static memberName(node) {
|
|
3386
|
+
const property = node.child("property");
|
|
3387
|
+
if (!node.flagAt("computed")) return property.name;
|
|
3388
|
+
return typeof property.literalValue === "string" ? property.literalValue : void 0;
|
|
3389
|
+
}
|
|
3390
|
+
/** The text of a string literal, or of a template literal with no placeholder; else `undefined`. */
|
|
3391
|
+
static literalText(node) {
|
|
3392
|
+
if (node === void 0 || !node.exists()) return;
|
|
3393
|
+
if (typeof node.literalValue === "string") return node.literalValue;
|
|
3394
|
+
const quasis = node.is(AstNodeType.TemplateLiteral) ? node.children("quasis") : [];
|
|
3395
|
+
return quasis.length === 1 && !node.hasChildren("expressions") ? quasis[0].child("value").stringAt("cooked") ?? void 0 : void 0;
|
|
3396
|
+
}
|
|
3397
|
+
/** Who a generated file belongs to: its module, or the file itself. */
|
|
3398
|
+
static ownerOf(file) {
|
|
3399
|
+
const moduleClass = PlatformBindingCensus.moduleInitializerPattern.exec(file.content)?.[1];
|
|
3400
|
+
return moduleClass === void 0 ? `generated file ${file.path}` : `module ${moduleClass}`;
|
|
3401
|
+
}
|
|
3402
|
+
/** The bindings a Cloudflare-targeted deploy-config fragment declares. */
|
|
3403
|
+
static declaredBy(contribution, platformName) {
|
|
3404
|
+
if (contribution.target !== DeployPlatform.Cloudflare || contribution.target !== platformName) return [];
|
|
3405
|
+
const declare = PlatformBindingCensus.declaringSections[contribution.section];
|
|
3406
|
+
const payload = contribution.payload;
|
|
3407
|
+
if (declare === void 0 || typeof payload !== "object" || payload === null) return [];
|
|
3408
|
+
return declare(payload);
|
|
3409
|
+
}
|
|
3410
|
+
/** The Durable Object bindings, each owned by its class. */
|
|
3411
|
+
static durableBindings(value) {
|
|
3412
|
+
if (!Array.isArray(value)) return [];
|
|
3413
|
+
const declared = [];
|
|
3414
|
+
for (const entry of value) {
|
|
3415
|
+
const record = entry;
|
|
3416
|
+
if (typeof record?.name === "string") declared.push({
|
|
3417
|
+
binding: record.name,
|
|
3418
|
+
neededBy: typeof record.class_name === "string" ? `Durable Object class ${record.class_name}` : "a Durable Object"
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
return declared;
|
|
3422
|
+
}
|
|
3423
|
+
/** The string `key` of each record in a payload list. */
|
|
3424
|
+
static entriesOf(value, key) {
|
|
3425
|
+
if (!Array.isArray(value)) return [];
|
|
3426
|
+
return value.flatMap((entry) => {
|
|
3427
|
+
const name = entry?.[key];
|
|
3428
|
+
return typeof name === "string" && name.length > 0 ? [name] : [];
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
};
|
|
3432
|
+
//#endregion
|
|
3433
|
+
//#region src/stdout-redirect.ts
|
|
3434
|
+
/**
|
|
3435
|
+
* Sends everything written to stdout to stderr for a while, so the log lines of an app that runs in this
|
|
3436
|
+
* process cannot mix with the JSON the command prints. The original writer stays reachable through
|
|
3437
|
+
* {@link write} for that JSON.
|
|
3438
|
+
*/
|
|
3439
|
+
var StdoutRedirect = class {
|
|
3440
|
+
original;
|
|
3441
|
+
/** Start the redirect. A second call does nothing. */
|
|
3442
|
+
start() {
|
|
3443
|
+
if (this.original !== void 0) return;
|
|
3444
|
+
this.original = process.stdout.write.bind(process.stdout);
|
|
3445
|
+
process.stdout.write = ((...parameters) => process.stderr.write(...parameters));
|
|
3446
|
+
}
|
|
3447
|
+
/** Write to the real stdout, redirected or not. */
|
|
3448
|
+
write(text) {
|
|
3449
|
+
(this.original ?? process.stdout.write.bind(process.stdout))(text);
|
|
3450
|
+
}
|
|
3451
|
+
/** Stop the redirect. */
|
|
3452
|
+
stop() {
|
|
3453
|
+
if (this.original !== void 0) {
|
|
3454
|
+
process.stdout.write = this.original;
|
|
3455
|
+
this.original = void 0;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
};
|
|
3459
|
+
//#endregion
|
|
3460
|
+
//#region src/task-target-resolver.ts
|
|
3461
|
+
/** Thrown when the names do not pick exactly one module and one task. The message is ready to print. */
|
|
3462
|
+
var TaskTargetError = class extends Error {
|
|
3463
|
+
constructor(message) {
|
|
3464
|
+
super(message);
|
|
3465
|
+
this.name = "TaskTargetError";
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
/**
|
|
3469
|
+
* Picks the module class and the task class that `heximon task run <ModuleClass> <TaskClass>` names, by class
|
|
3470
|
+
* name, in the compiled graph. A name that matches nothing or more than one class fails with the names that
|
|
3471
|
+
* come close.
|
|
3472
|
+
*/
|
|
3473
|
+
var TaskTargetResolver = class TaskTargetResolver {
|
|
3474
|
+
/** The most edits between a typed name and a suggestion. */
|
|
3475
|
+
static maximumSuggestionDistance = 3;
|
|
3476
|
+
/** The most names a message lists. */
|
|
3477
|
+
static maximumListed = 8;
|
|
3478
|
+
/**
|
|
3479
|
+
* @param graph - The module graph of the production compile.
|
|
3480
|
+
* @param moduleName - The module class name.
|
|
3481
|
+
* @param taskName - The task class name; the module must provide it.
|
|
3482
|
+
* @returns The module and the task.
|
|
3483
|
+
* @throws {TaskTargetError} When a name matches no class, or more than one.
|
|
3484
|
+
*/
|
|
3485
|
+
static resolve(graph, moduleName, taskName) {
|
|
3486
|
+
const modules = TaskTargetResolver.distinct(graph.modules.map((node) => node.reference), moduleName);
|
|
3487
|
+
if (modules.length === 0) throw new TaskTargetError(`No module class named "${moduleName}" is in the compiled graph.` + TaskTargetResolver.suggestions(moduleName, graph.modules.map((node) => TaskTargetResolver.nameOf(node.reference)), "Modules in the graph"));
|
|
3488
|
+
if (modules.length > 1) throw new TaskTargetError(`More than one module class is named "${moduleName}":\n` + modules.map((reference) => ` - ${reference.sourceFile}`).join("\n"));
|
|
3489
|
+
const module = modules[0];
|
|
3490
|
+
const nodes = graph.modules.filter((node) => TaskTargetResolver.isSame(node.reference, module));
|
|
3491
|
+
const providerTokens = nodes.flatMap((node) => node.providers.map((provider) => provider.token));
|
|
3492
|
+
const tasks = TaskTargetResolver.distinct(providerTokens, taskName);
|
|
3493
|
+
if (tasks.length === 0) throw new TaskTargetError(`The module ${moduleName} does not provide a class named "${taskName}".` + TaskTargetResolver.otherOwner(graph, nodes, taskName) + TaskTargetResolver.suggestions(taskName, providerTokens.map((token) => TaskTargetResolver.nameOf(token)), `Providers of ${moduleName}`));
|
|
3494
|
+
if (tasks.length > 1) throw new TaskTargetError(`The module ${moduleName} provides more than one class named "${taskName}":\n` + tasks.map((reference) => ` - ${reference.sourceFile}`).join("\n"));
|
|
3495
|
+
return {
|
|
3496
|
+
module,
|
|
3497
|
+
task: tasks[0]
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
/** The class name a person typed: the declared name when the compiler aliased the export. */
|
|
3501
|
+
static nameOf(reference) {
|
|
3502
|
+
return reference.displayName ?? reference.className;
|
|
3503
|
+
}
|
|
3504
|
+
/** Whether two references name the same class. */
|
|
3505
|
+
static isSame(left, right) {
|
|
3506
|
+
return left.sourceFile === right.sourceFile && left.className === right.className;
|
|
3507
|
+
}
|
|
3508
|
+
/** The references that carry `name`, one for each class, in graph order. */
|
|
3509
|
+
static distinct(references, name) {
|
|
3510
|
+
const found = [];
|
|
3511
|
+
for (const reference of references) if (TaskTargetResolver.nameOf(reference) === name && !found.some((known) => TaskTargetResolver.isSame(known, reference))) found.push(reference);
|
|
3512
|
+
return found;
|
|
3513
|
+
}
|
|
3514
|
+
/** A hint when another module of the graph provides the class. */
|
|
3515
|
+
static otherOwner(graph, own, taskName) {
|
|
3516
|
+
const owners = graph.modules.filter((node) => !own.includes(node) && node.providers.some((provider) => TaskTargetResolver.nameOf(provider.token) === taskName));
|
|
3517
|
+
if (owners.length === 0) return "";
|
|
3518
|
+
return ` The class is provided by ${[...new Set(owners.map((node) => TaskTargetResolver.nameOf(node.reference)))].join(", ")}.`;
|
|
3519
|
+
}
|
|
3520
|
+
/** The "did you mean" line, or the start of the list when nothing is close. */
|
|
3521
|
+
static suggestions(typed, candidates, listTitle) {
|
|
3522
|
+
const unique = [...new Set(candidates)];
|
|
3523
|
+
const close = unique.filter((candidate) => candidate.toLowerCase().includes(typed.toLowerCase()) || typed.toLowerCase().includes(candidate.toLowerCase()) || TaskTargetResolver.distance(candidate.toLowerCase(), typed.toLowerCase()) <= TaskTargetResolver.maximumSuggestionDistance);
|
|
3524
|
+
if (close.length > 0) return ` Did you mean ${close.slice(0, TaskTargetResolver.maximumListed).map((name) => `"${name}"`).join(", ")}?`;
|
|
3525
|
+
const listed = unique.slice(0, TaskTargetResolver.maximumListed);
|
|
3526
|
+
return listed.length === 0 ? "" : ` ${listTitle}: ${listed.join(", ")}${unique.length > listed.length ? ", …" : ""}.`;
|
|
3527
|
+
}
|
|
3528
|
+
/** The Levenshtein distance between two names. */
|
|
3529
|
+
static distance(left, right) {
|
|
3530
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
3531
|
+
for (let row = 1; row <= left.length; row += 1) {
|
|
3532
|
+
const current = [row];
|
|
3533
|
+
for (let column = 1; column <= right.length; column += 1) {
|
|
3534
|
+
const substitution = left[row - 1] === right[column - 1] ? 0 : 1;
|
|
3535
|
+
current[column] = Math.min((previous[column] ?? 0) + 1, (current[column - 1] ?? 0) + 1, (previous[column - 1] ?? 0) + substitution);
|
|
3536
|
+
}
|
|
3537
|
+
previous = current;
|
|
3538
|
+
}
|
|
3539
|
+
return previous[right.length] ?? 0;
|
|
3540
|
+
}
|
|
3541
|
+
};
|
|
3542
|
+
//#endregion
|
|
3543
|
+
//#region src/tsconfig-aliases.ts
|
|
3544
|
+
/**
|
|
3545
|
+
* Reads the module aliases of a Nitro project from the `paths` of its tsconfig files. Nitro writes the aliases
|
|
3546
|
+
* of a project into a generated tsconfig, and the project's tsconfig extends it and adds its own. The
|
|
3547
|
+
* files are read as they are on disk: nothing is generated, and no config of the project runs.
|
|
3548
|
+
*
|
|
3549
|
+
* TypeScript's rules apply. A `paths` of a config replaces the `paths` of what it extends. `baseUrl` is the
|
|
3550
|
+
* nearest one in the chain, and a target is relative to it; without one, a target is relative to the directory
|
|
3551
|
+
* of the config that sets `paths`. An exact pattern wins over a wildcard one, and a longer wildcard prefix wins
|
|
3552
|
+
* over a shorter one.
|
|
3553
|
+
*/
|
|
3554
|
+
var TsconfigAliases = class TsconfigAliases {
|
|
3555
|
+
/** Where Nitro writes its generated tsconfig, below the build directory. */
|
|
3556
|
+
static generatedFiles = ["node_modules/.nitro/types/tsconfig.json", ".nitro/types/tsconfig.json"];
|
|
3557
|
+
/** The longest `extends` chain followed. */
|
|
3558
|
+
static maximumDepth = 8;
|
|
3559
|
+
/**
|
|
3560
|
+
* The aliases of the project. The project's tsconfig wins over the generated one that it does not extend.
|
|
3561
|
+
*
|
|
3562
|
+
* @param rootDir - The project root.
|
|
3563
|
+
*/
|
|
3564
|
+
static read(rootDir) {
|
|
3565
|
+
const exact = {};
|
|
3566
|
+
const prefixes = {};
|
|
3567
|
+
for (const file of [...TsconfigAliases.generatedFiles.map((name) => join(rootDir, name)), join(rootDir, "tsconfig.json")]) {
|
|
3568
|
+
const options = TsconfigAliases.optionsOf(file, 0);
|
|
3569
|
+
if (options.paths === void 0) continue;
|
|
3570
|
+
const base = options.baseUrl ?? options.paths.directory;
|
|
3571
|
+
for (const [pattern, targets] of Object.entries(options.paths.entries)) {
|
|
3572
|
+
const target = targets[0];
|
|
3573
|
+
if (target === void 0) continue;
|
|
3574
|
+
if (pattern.endsWith("/*") && target.endsWith("/*")) prefixes[pattern.slice(0, -2)] = resolve(base, target.slice(0, -2));
|
|
3575
|
+
else if (!pattern.includes("*") && !target.includes("*")) exact[pattern] = resolve(base, target);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
return {
|
|
3579
|
+
exact,
|
|
3580
|
+
prefixes
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
/** The effective options of one tsconfig, after the files it extends. */
|
|
3584
|
+
static optionsOf(file, depth) {
|
|
3585
|
+
if (!existsSync(file) || depth > TsconfigAliases.maximumDepth) return {};
|
|
3586
|
+
let config;
|
|
3587
|
+
try {
|
|
3588
|
+
config = JSON.parse(TsconfigAliases.toJson(readFileSync(file, "utf8")));
|
|
3589
|
+
} catch {
|
|
3590
|
+
return {};
|
|
3591
|
+
}
|
|
3592
|
+
const options = {};
|
|
3593
|
+
const parents = typeof config.extends === "string" ? [config.extends] : [...config.extends ?? []];
|
|
3594
|
+
for (const parent of parents) if (parent.startsWith(".") || isAbsolute(parent)) {
|
|
3595
|
+
const path = resolve(dirname(file), parent);
|
|
3596
|
+
const inherited = TsconfigAliases.optionsOf(path.endsWith(".json") ? path : `${path}.json`, depth + 1);
|
|
3597
|
+
Object.assign(options, inherited);
|
|
3598
|
+
}
|
|
3599
|
+
if (config.compilerOptions?.baseUrl !== void 0) options.baseUrl = resolve(dirname(file), config.compilerOptions.baseUrl);
|
|
3600
|
+
if (config.compilerOptions?.paths !== void 0) options.paths = {
|
|
3601
|
+
entries: config.compilerOptions.paths,
|
|
3602
|
+
directory: dirname(file)
|
|
3603
|
+
};
|
|
3604
|
+
return options;
|
|
3605
|
+
}
|
|
3606
|
+
/**
|
|
3607
|
+
* A tsconfig is JSON with comments and trailing commas. This removes both and leaves every string as it is,
|
|
3608
|
+
* so `JSON.parse` can read the text.
|
|
3609
|
+
*/
|
|
3610
|
+
static toJson(text) {
|
|
3611
|
+
let withoutComments = "";
|
|
3612
|
+
let inString = false;
|
|
3613
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
3614
|
+
const character = text[index];
|
|
3615
|
+
if (inString) {
|
|
3616
|
+
withoutComments += character;
|
|
3617
|
+
if (character === "\\") withoutComments += text[++index] ?? "";
|
|
3618
|
+
else if (character === "\"") inString = false;
|
|
3619
|
+
} else if (character === "\"") {
|
|
3620
|
+
inString = true;
|
|
3621
|
+
withoutComments += character;
|
|
3622
|
+
} else if (character === "/" && text[index + 1] === "/") {
|
|
3623
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
3624
|
+
withoutComments += "\n";
|
|
3625
|
+
} else if (character === "/" && text[index + 1] === "*") {
|
|
3626
|
+
index = text.indexOf("*/", index + 2);
|
|
3627
|
+
index = index === -1 ? text.length : index + 1;
|
|
3628
|
+
} else withoutComments += character;
|
|
3629
|
+
}
|
|
3630
|
+
let json = "";
|
|
3631
|
+
inString = false;
|
|
3632
|
+
for (let index = 0; index < withoutComments.length; index += 1) {
|
|
3633
|
+
const character = withoutComments[index];
|
|
3634
|
+
if (inString) {
|
|
3635
|
+
json += character;
|
|
3636
|
+
if (character === "\\") json += withoutComments[++index] ?? "";
|
|
3637
|
+
else if (character === "\"") inString = false;
|
|
3638
|
+
} else if (character === "\"") {
|
|
3639
|
+
inString = true;
|
|
3640
|
+
json += character;
|
|
3641
|
+
} else if (character === "," && /^\s*[}\]]/.test(withoutComments.slice(index + 1))) continue;
|
|
3642
|
+
else json += character;
|
|
3643
|
+
}
|
|
3644
|
+
return json;
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
//#endregion
|
|
3648
|
+
//#region src/task-runner.ts
|
|
3649
|
+
/** The in-process client of a run. A run serves no HTTP, so every call fails with the reason. */
|
|
3650
|
+
var RefusingInternalClient = class {
|
|
3651
|
+
fetch(input) {
|
|
3652
|
+
return Promise.reject(/* @__PURE__ */ new Error(`InternalClient.fetch(${JSON.stringify(input)}) is not available in \`heximon task run\`: the run boots no HTTP surface, so a task cannot call the app's own routes.`));
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
/** The failure a run reports to the caller; the message is ready to print. */
|
|
3656
|
+
var TaskRunFailure = class extends Error {};
|
|
3657
|
+
/**
|
|
3658
|
+
* `heximon task run`: compiles the production graph into a temporary directory, boots it in this process,
|
|
3659
|
+
* runs one {@link Task}, prints its result as JSON, and removes the compile output.
|
|
3660
|
+
*
|
|
3661
|
+
* The boot starts no HTTP listener and none of the standing work of the deployed app — no schedule timers,
|
|
3662
|
+
* queue consumers, pollers or sweeps, and no outbox relay unless the caller asked for it — so a run delivers
|
|
3663
|
+
* nothing of what the deployed app has pending. The result goes to stdout; everything else goes to stderr, the
|
|
3664
|
+
* log lines of the app included.
|
|
3665
|
+
*/
|
|
3666
|
+
var TaskRunner = class TaskRunner {
|
|
3667
|
+
options;
|
|
3668
|
+
/** The name prefix of the temporary compile output; the workspace ignore globs match it. */
|
|
3669
|
+
static outputDirectoryPrefix = ".heximon-task-";
|
|
3670
|
+
/** The most the run waits for in-flight background work before it shuts the app down. */
|
|
3671
|
+
static drainTimeoutMilliseconds = 1e4;
|
|
3672
|
+
/** The most the interrupt handler waits for the app to shut down. */
|
|
3673
|
+
static interruptShutdownMilliseconds = 5e3;
|
|
3674
|
+
static wiringFilePattern = /^apps\/.+\.wiring\.js$/;
|
|
3675
|
+
outputDirectory;
|
|
3676
|
+
app;
|
|
3677
|
+
context;
|
|
3678
|
+
platformName;
|
|
3679
|
+
/** The Nitro alias table of the project; empty for any other project. */
|
|
3680
|
+
aliases = {
|
|
3681
|
+
exact: {},
|
|
3682
|
+
prefixes: {}
|
|
3683
|
+
};
|
|
3684
|
+
runtime;
|
|
3685
|
+
restoreStandingWork;
|
|
3686
|
+
restoreHostBindings;
|
|
3687
|
+
stdout = new StdoutRedirect();
|
|
3688
|
+
/** The result of the task as one line of JSON, printed after the shutdown. */
|
|
3689
|
+
serialized;
|
|
3690
|
+
disposing;
|
|
3691
|
+
constructor(options) {
|
|
3692
|
+
this.options = options;
|
|
3693
|
+
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Run the task.
|
|
3696
|
+
*
|
|
3697
|
+
* @returns The process exit code: `0` when the task returned, `1` for every other outcome. The reason is
|
|
3698
|
+
* already on stderr.
|
|
3699
|
+
*/
|
|
3700
|
+
async run() {
|
|
3701
|
+
let interrupts = 0;
|
|
3702
|
+
const interrupt = (signal) => {
|
|
3703
|
+
interrupts += 1;
|
|
3704
|
+
this.interrupted(signal, interrupts > 1);
|
|
3705
|
+
};
|
|
3706
|
+
process.on("SIGINT", interrupt);
|
|
3707
|
+
process.on("SIGTERM", interrupt);
|
|
3708
|
+
let exitCode = 0;
|
|
3709
|
+
this.stdout.start();
|
|
3710
|
+
try {
|
|
3711
|
+
this.outputDirectory = mkdtempSync(join(this.options.rootDir, TaskRunner.outputDirectoryPrefix));
|
|
3712
|
+
this.runtime = await TaskRunner.projectRuntime(this.outputDirectory);
|
|
3713
|
+
const output = await this.compile();
|
|
3714
|
+
const target = this.pickTarget(output);
|
|
3715
|
+
TaskRunner.refuseBindings(output, this.platformName, this.outputDirectory);
|
|
3716
|
+
this.materialize(output);
|
|
3717
|
+
await this.boot(output);
|
|
3718
|
+
await this.execute(target);
|
|
3719
|
+
} catch (error) {
|
|
3720
|
+
TaskRunner.report(error);
|
|
3721
|
+
exitCode = 1;
|
|
3722
|
+
} finally {
|
|
3723
|
+
if (!await this.dispose()) exitCode = 1;
|
|
3724
|
+
this.stdout.stop();
|
|
3725
|
+
if (this.serialized !== void 0) this.stdout.write(`${this.serialized}\n`);
|
|
3726
|
+
process.off("SIGINT", interrupt);
|
|
3727
|
+
process.off("SIGTERM", interrupt);
|
|
3728
|
+
}
|
|
3729
|
+
return exitCode;
|
|
3730
|
+
}
|
|
3731
|
+
/** The `@heximon/runtime` of the project, the copy the generated wiring imports. */
|
|
3732
|
+
static async projectRuntime(directory) {
|
|
3733
|
+
const runtime = await ProjectModules.runtime(directory);
|
|
3734
|
+
if (typeof runtime.StandingWork?.suspend !== "function") throw new TaskRunFailure("The @heximon/runtime of this project is older than this command. Update @heximon/runtime and heximon to one version.");
|
|
3735
|
+
return runtime;
|
|
3736
|
+
}
|
|
3737
|
+
/**
|
|
3738
|
+
* Compile the production graph into the temporary directory. A Nitro host compiles with the inputs of its
|
|
3739
|
+
* own build (the preset, the plugins, the source directories); any other project with its sidecar.
|
|
3740
|
+
*/
|
|
3741
|
+
async compile() {
|
|
3742
|
+
const rootDir = this.options.rootDir;
|
|
3743
|
+
const outputDirectory = this.outputDirectory;
|
|
3744
|
+
let output;
|
|
3745
|
+
let compiler;
|
|
3746
|
+
const nitro = NitroStaticConfig.read(rootDir);
|
|
3747
|
+
this.aliases = nitro.isNitroHost ? TsconfigAliases.read(rootDir) : {
|
|
3748
|
+
exact: {},
|
|
3749
|
+
prefixes: {}
|
|
3750
|
+
};
|
|
3751
|
+
if (nitro.isNitroHost) {
|
|
3752
|
+
const sourceDirectories = TaskRunner.sourceDirectoriesOf(nitro, this.options.isSourceExplicit === true ? this.options.sourceDirectories ?? [] : void 0);
|
|
3753
|
+
const preset = nitro.preset;
|
|
3754
|
+
const host = await NitroHost.open(rootDir, outputDirectory, {
|
|
3755
|
+
preset: preset.kind === "literal" ? preset.value : void 0,
|
|
3756
|
+
platformFromSidecar: preset.kind === "computed",
|
|
3757
|
+
aliases: this.aliases,
|
|
3758
|
+
...sourceDirectories
|
|
3759
|
+
});
|
|
3760
|
+
if (preset.kind === "computed" && !host.hasSidecarPlatform()) throw new TaskRunFailure(`The Nitro preset is computed (${preset.reason}, in ${preset.file}), and the command does not run your config files, so it cannot read the platform: declare \`platform\` in heximon.config.ts (for example \`platform: new CloudflareWorkersStrategy()\`), or set NITRO_PRESET. Nothing was booted.`);
|
|
3761
|
+
const compiled = host.compile(rootDir, outputDirectory, this.options.isSourceExplicit === true ? this.options.sourceDirectories : void 0);
|
|
3762
|
+
({output, compiler} = compiled);
|
|
3763
|
+
this.platformName = compiled.platformName;
|
|
3764
|
+
} else {
|
|
3765
|
+
const session = await ProjectCompileSession.open({
|
|
3766
|
+
rootDir,
|
|
3767
|
+
environment: "production",
|
|
3768
|
+
...this.options.sourceDirectories === void 0 ? {} : { sourceDirectories: this.options.sourceDirectories }
|
|
3769
|
+
});
|
|
3770
|
+
this.platformName = session.platformName();
|
|
3771
|
+
output = await session.compileProduction(outputDirectory, this.aliases);
|
|
3772
|
+
compiler = session.compiler;
|
|
3773
|
+
}
|
|
3774
|
+
const errors = output.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
3775
|
+
if (output.diagnostics.length > 0) CompileDiagnostics.renderToStderr(rootDir, output.diagnostics);
|
|
3776
|
+
if (errors.length > 0) throw new TaskRunFailure(`The production graph has ${errors.length} compile error${errors.length === 1 ? "" : "s"}. Nothing was booted.`);
|
|
3777
|
+
const findings = LibraryFactoryPreflight.inspect(compiler.consumedBoundaryPackages(), LibraryFactoryPreflight.productionBuildOptions);
|
|
3778
|
+
if (findings.length > 0) throw new TaskRunFailure(LibraryFactoryPreflight.explain(findings));
|
|
3779
|
+
return output;
|
|
3780
|
+
}
|
|
3781
|
+
/**
|
|
3782
|
+
* The literal source directories of the Nitro config, for the host inputs. A value that is not a literal is
|
|
3783
|
+
* refused, because dropping it would compile a smaller graph; `--src` names the directories instead.
|
|
3784
|
+
*/
|
|
3785
|
+
static sourceDirectoriesOf(nitro, explicit) {
|
|
3786
|
+
if (explicit !== void 0) return { scanDirs: explicit };
|
|
3787
|
+
for (const [name, value] of [["serverDir", nitro.serverDir], ["scanDirs", nitro.scanDirs]]) if (value.kind === "computed") throw new TaskRunFailure(`The Nitro \`${name}\` is computed (${value.reason}, in ${value.file}), and the command does not run your config files, so it cannot read the source directories. Name them with \`--src <dir>\` (repeat the option, or separate directories with commas). Nothing was booted.`);
|
|
3788
|
+
return {
|
|
3789
|
+
...nitro.serverDir.kind === "literal" ? { serverDir: nitro.serverDir.value } : {},
|
|
3790
|
+
...nitro.scanDirs.kind === "literal" ? { scanDirs: nitro.scanDirs.value } : {}
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
/** Pick the module and the task from the compiled graph. */
|
|
3794
|
+
pickTarget(output) {
|
|
3795
|
+
if (output.moduleGraph === void 0) throw new TaskRunFailure("The compile produced no module graph, so the task cannot be found.");
|
|
3796
|
+
return TaskTargetResolver.resolve(output.moduleGraph, this.options.moduleName, this.options.taskName);
|
|
3797
|
+
}
|
|
3798
|
+
/** Refuse a graph that needs bindings only its platform has, before anything boots. */
|
|
3799
|
+
static refuseBindings(output, platformName, outputDirectory) {
|
|
3800
|
+
const needs = PlatformBindingCensus.collect(output, platformName, outputDirectory);
|
|
3801
|
+
if (needs.length > 0) throw new TaskRunFailure(PlatformBindingCensus.explain(needs, platformName));
|
|
3802
|
+
}
|
|
3803
|
+
/** Write the generated files into the temporary directory. */
|
|
3804
|
+
materialize(output) {
|
|
3805
|
+
for (const file of output.files) {
|
|
3806
|
+
const target = join(this.outputDirectory, file.path);
|
|
3807
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
3808
|
+
writeFileSync(target, file.content);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
/** Boot the app with no HTTP surface and without the standing work of the deployed app. */
|
|
3812
|
+
async boot(output) {
|
|
3813
|
+
const wiring = output.files.find((file) => TaskRunner.wiringFilePattern.test(file.path));
|
|
3814
|
+
if (wiring === void 0) throw new TaskRunFailure("No app wiring file was generated. Make sure the project has a root module (a module that no other module imports).");
|
|
3815
|
+
const runtime = this.runtime;
|
|
3816
|
+
if (TransformBoot.isBootProcess()) TransformBoot.registerSourceResolver(new Map(output.sourceRewrites.map((rewrite) => [rewrite.filePath, rewrite.content])), this.aliases);
|
|
3817
|
+
runtime.StandingWork.suspend(runtime.StandingWork.kinds().filter((kind) => !(this.options.withOutboxRelay && kind === runtime.StandingWorkKind.OutboxRelay)));
|
|
3818
|
+
this.restoreStandingWork = () => runtime.StandingWork.suspend([]);
|
|
3819
|
+
runtime.Platform.capture(process.env);
|
|
3820
|
+
const runtimeConfigFile = output.files.find((file) => file.path === RuntimeConfigFile.path);
|
|
3821
|
+
const source = (runtimeConfigFile === void 0 ? void 0 : await import(pathToFileURL(join(this.outputDirectory, runtimeConfigFile.path)).href))?.[RuntimeConfigFile.sourceExport];
|
|
3822
|
+
const coreConfig = source === void 0 ? new runtime.CoreConfig(process.env) : runtime.CoreConfig.resolve(process.env, source);
|
|
3823
|
+
runtime.Logger.configure(coreConfig.logger);
|
|
3824
|
+
const wiringModule = await import(pathToFileURL(join(this.outputDirectory, wiring.path)).href);
|
|
3825
|
+
const bindings = {
|
|
3826
|
+
InternalClient: new RefusingInternalClient(),
|
|
3827
|
+
CoreConfig: coreConfig
|
|
3828
|
+
};
|
|
3829
|
+
runtime.HostBindings.capture(bindings);
|
|
3830
|
+
this.restoreHostBindings = () => runtime.HostBindings.capture({});
|
|
3831
|
+
this.app = await wiringModule.createApp(bindings);
|
|
3832
|
+
this.context = await this.app.from(runtime.CoreModule).get(runtime.Context);
|
|
3833
|
+
}
|
|
3834
|
+
/** Run the task and print its result. */
|
|
3835
|
+
async execute(target) {
|
|
3836
|
+
const runtime = this.runtime;
|
|
3837
|
+
const moduleClass = await this.importClass(target.module.sourceFile, target.module.className);
|
|
3838
|
+
const taskClass = await this.importClass(target.task.sourceFile, target.task.className);
|
|
3839
|
+
if (typeof taskClass.prototype.run !== "function") throw new TaskRunFailure(`${this.options.taskName} is not a task: the class has no run() method. A task extends Task.`);
|
|
3840
|
+
const manager = new runtime.TaskManager(this.app, this.context, await this.seedsOf(runtime));
|
|
3841
|
+
let outcome;
|
|
3842
|
+
try {
|
|
3843
|
+
outcome = await manager.from(moduleClass).run(taskClass, {
|
|
3844
|
+
name: this.options.taskName,
|
|
3845
|
+
...this.options.payload === void 0 ? {} : { payload: this.options.payload }
|
|
3846
|
+
});
|
|
3847
|
+
} catch (error) {
|
|
3848
|
+
throw new TaskRunFailure(`The task ${this.options.taskName} failed:\n${error instanceof Error ? error.stack ?? error.message : String(error)}`, { cause: error });
|
|
3849
|
+
}
|
|
3850
|
+
try {
|
|
3851
|
+
this.serialized = JSON.stringify(outcome);
|
|
3852
|
+
} catch (error) {
|
|
3853
|
+
throw new TaskRunFailure(`The task ran, but its result is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
/** The seeds the app declares for every background frame, or none. */
|
|
3857
|
+
async seedsOf(runtime) {
|
|
3858
|
+
const resolvable = this.app;
|
|
3859
|
+
try {
|
|
3860
|
+
return await resolvable.resolveUnscoped?.(runtime.ContextSeeds);
|
|
3861
|
+
} catch (error) {
|
|
3862
|
+
if (error instanceof Error && error.message.startsWith("No provider for")) return;
|
|
3863
|
+
throw error;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
/** Import a class by file (or package specifier) and export name. */
|
|
3867
|
+
async importClass(sourceFile, className) {
|
|
3868
|
+
const found = (await (isAbsolute(sourceFile) ? import(pathToFileURL(sourceFile).href) : import(sourceFile)))[className];
|
|
3869
|
+
if (found === void 0) throw new TaskRunFailure(`${sourceFile} does not export a class named ${className}.`);
|
|
3870
|
+
return found;
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Shut the app down and remove the compile output. Safe to call twice.
|
|
3874
|
+
*
|
|
3875
|
+
* @returns `false` when the shutdown failed. The reason is already on stderr.
|
|
3876
|
+
*/
|
|
3877
|
+
dispose() {
|
|
3878
|
+
this.disposing ??= this.disposeOnce();
|
|
3879
|
+
return this.disposing;
|
|
3880
|
+
}
|
|
3881
|
+
async disposeOnce() {
|
|
3882
|
+
let clean = true;
|
|
3883
|
+
try {
|
|
3884
|
+
await this.runtime?.StandingWork.settled({ timeoutMs: TaskRunner.drainTimeoutMilliseconds });
|
|
3885
|
+
await this.context?.drain({ timeoutMs: TaskRunner.drainTimeoutMilliseconds });
|
|
3886
|
+
await this.app?.shutdown();
|
|
3887
|
+
} catch (error) {
|
|
3888
|
+
clean = false;
|
|
3889
|
+
process.stderr.write(`The task finished, but shutting the app down failed — a client may still be open: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
3890
|
+
}
|
|
3891
|
+
this.restoreStandingWork?.();
|
|
3892
|
+
this.restoreStandingWork = void 0;
|
|
3893
|
+
this.restoreHostBindings?.();
|
|
3894
|
+
this.restoreHostBindings = void 0;
|
|
3895
|
+
if (this.outputDirectory !== void 0) {
|
|
3896
|
+
rmSync(this.outputDirectory, {
|
|
3897
|
+
recursive: true,
|
|
3898
|
+
force: true
|
|
3899
|
+
});
|
|
3900
|
+
this.outputDirectory = void 0;
|
|
3901
|
+
}
|
|
3902
|
+
return clean;
|
|
3903
|
+
}
|
|
3904
|
+
/** An interrupt: shut down within a budget, remove the output, and leave with the signal's code. */
|
|
3905
|
+
async interrupted(signal, immediately) {
|
|
3906
|
+
process.stderr.write(`\nReceived ${signal}: shutting down and removing the compile output.\n`);
|
|
3907
|
+
if (!immediately) {
|
|
3908
|
+
const budget = new Promise((resolve) => {
|
|
3909
|
+
setTimeout(resolve, TaskRunner.interruptShutdownMilliseconds).unref();
|
|
3910
|
+
});
|
|
3911
|
+
await Promise.race([this.dispose(), budget]);
|
|
3912
|
+
}
|
|
3913
|
+
if (this.outputDirectory !== void 0) rmSync(this.outputDirectory, {
|
|
3914
|
+
recursive: true,
|
|
3915
|
+
force: true
|
|
3916
|
+
});
|
|
3917
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
3918
|
+
}
|
|
3919
|
+
/** Print a failure on stderr. */
|
|
3920
|
+
static report(error) {
|
|
3921
|
+
if (error instanceof TaskRunFailure || error instanceof TaskTargetError) {
|
|
3922
|
+
process.stderr.write(`${error.message}\n`);
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
const text = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
3926
|
+
process.stderr.write(`${text}\n`);
|
|
3927
|
+
}
|
|
3928
|
+
};
|
|
3929
|
+
//#endregion
|
|
3930
|
+
//#region src/task-command.ts
|
|
3931
|
+
/** Helpers of {@link taskCommand}: the argument checks, the environment file and the exit. */
|
|
3932
|
+
var TaskCommand = class TaskCommand {
|
|
3933
|
+
/**
|
|
3934
|
+
* The longest a finished run may keep the process alive. A task can leave a handle open (a socket, a
|
|
3935
|
+
* timer) that its app did not close, and a one-off run must end.
|
|
3936
|
+
*/
|
|
3937
|
+
static exitGraceMilliseconds = 1e4;
|
|
3938
|
+
/**
|
|
3939
|
+
* Read `--payload`. It must be a JSON object, because it becomes the payload of the task's event.
|
|
3940
|
+
*
|
|
3941
|
+
* @param raw - The raw `--payload` text, when the caller passed one.
|
|
3942
|
+
*/
|
|
3943
|
+
static parsePayload(raw) {
|
|
3944
|
+
if (raw === void 0) return {
|
|
3945
|
+
ok: true,
|
|
3946
|
+
payload: void 0
|
|
3947
|
+
};
|
|
3948
|
+
let parsed;
|
|
3949
|
+
try {
|
|
3950
|
+
parsed = JSON.parse(raw);
|
|
3951
|
+
} catch (error) {
|
|
3952
|
+
return {
|
|
3953
|
+
ok: false,
|
|
3954
|
+
reason: `--payload is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {
|
|
3958
|
+
ok: false,
|
|
3959
|
+
reason: "--payload must be a JSON object, such as '{\"year\": 2026}'."
|
|
3960
|
+
};
|
|
3961
|
+
return {
|
|
3962
|
+
ok: true,
|
|
3963
|
+
payload: parsed
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Put the variables of an env file into `process.env`. A variable that the shell already sets keeps the
|
|
3968
|
+
* shell's value.
|
|
3969
|
+
*
|
|
3970
|
+
* @param filePath - The absolute path of a dotenv-format file.
|
|
3971
|
+
* @throws {EnvFileUnreadableError} When the file cannot be read.
|
|
3972
|
+
*/
|
|
3973
|
+
static async loadEnvironmentFile(filePath) {
|
|
3974
|
+
for (const [name, value] of await EnvFileReader.read(filePath)) if (process.env[name] === void 0) process.env[name] = value;
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Every `--src` of the raw arguments, as one comma-separated text. The argument parser keeps only the last
|
|
3978
|
+
* value of a repeated option, so the values are collected here, before it parses: `--src server --src extra`,
|
|
3979
|
+
* `--src=server`, and `--src server,extra` all count.
|
|
3980
|
+
*
|
|
3981
|
+
* @param rawArgs - The arguments after `task run`.
|
|
3982
|
+
* @returns The text, or `undefined` when the option is absent.
|
|
3983
|
+
*/
|
|
3984
|
+
static sourceOption(rawArgs) {
|
|
3985
|
+
const values = [];
|
|
3986
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
3987
|
+
const argument = rawArgs[index];
|
|
3988
|
+
if (argument === "--") break;
|
|
3989
|
+
if (argument === "--src") {
|
|
3990
|
+
const value = rawArgs[index + 1];
|
|
3991
|
+
if (value !== void 0 && !value.startsWith("--")) {
|
|
3992
|
+
values.push(value);
|
|
3993
|
+
index += 1;
|
|
3994
|
+
}
|
|
3995
|
+
} else if (argument.startsWith("--src=")) values.push(argument.slice(6));
|
|
3996
|
+
}
|
|
3997
|
+
return values.length === 0 ? void 0 : values.join(",");
|
|
3998
|
+
}
|
|
3999
|
+
/**
|
|
4000
|
+
* Set the exit code and make sure the process ends soon after, even when the task left a handle open.
|
|
4001
|
+
* The timer does not keep the process alive by itself.
|
|
4002
|
+
*/
|
|
4003
|
+
static finish(exitCode) {
|
|
4004
|
+
process.exitCode = exitCode;
|
|
4005
|
+
setTimeout(() => process.exit(exitCode), TaskCommand.exitGraceMilliseconds).unref();
|
|
4006
|
+
}
|
|
4007
|
+
/** Print a usage failure and set the exit code. */
|
|
4008
|
+
static refuse(reason) {
|
|
4009
|
+
process.stderr.write(`heximon task run: ${reason}\n`);
|
|
4010
|
+
process.exitCode = 1;
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
const taskRunCommand = defineCommand({
|
|
4014
|
+
meta: {
|
|
4015
|
+
name: "run",
|
|
4016
|
+
description: "Run one Task against the production graph of the app, then exit. Prints the result as JSON on stdout."
|
|
4017
|
+
},
|
|
4018
|
+
args: {
|
|
4019
|
+
module: {
|
|
4020
|
+
type: "positional",
|
|
4021
|
+
description: "The class name of the module that declares the task.",
|
|
4022
|
+
required: true
|
|
4023
|
+
},
|
|
4024
|
+
task: {
|
|
4025
|
+
type: "positional",
|
|
4026
|
+
description: "The class name of the task.",
|
|
4027
|
+
required: true
|
|
4028
|
+
},
|
|
4029
|
+
cwd: {
|
|
4030
|
+
type: "string",
|
|
4031
|
+
description: "Project root directory.",
|
|
4032
|
+
default: "."
|
|
4033
|
+
},
|
|
4034
|
+
payload: {
|
|
4035
|
+
type: "string",
|
|
4036
|
+
description: "A JSON object handed to the task as the payload of its event."
|
|
4037
|
+
},
|
|
4038
|
+
"env-file": {
|
|
4039
|
+
type: "string",
|
|
4040
|
+
description: "A dotenv-format file loaded before the compile. A variable the shell already sets keeps its value."
|
|
4041
|
+
},
|
|
4042
|
+
"with-outbox-relay": {
|
|
4043
|
+
type: "boolean",
|
|
4044
|
+
description: "Start the outbox relay. It delivers every pending row of the database, the rows of the deployed app included.",
|
|
4045
|
+
default: false
|
|
4046
|
+
},
|
|
4047
|
+
src: {
|
|
4048
|
+
type: "string",
|
|
4049
|
+
description: "Source directories to scan: repeat the option, or separate directories with commas. Defaults to `src/`, or the directories of the Nitro config for a Nitro host."
|
|
4050
|
+
}
|
|
4051
|
+
},
|
|
4052
|
+
async run({ args, rawArgs }) {
|
|
4053
|
+
if (!TransformBoot.isBootProcess()) {
|
|
4054
|
+
process.exitCode = await TransformBoot.reexecUnderTransform("task run");
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
const rootDir = resolve(process.cwd(), args.cwd);
|
|
4058
|
+
const parsed = TaskCommand.parsePayload(args.payload);
|
|
4059
|
+
if (!parsed.ok) {
|
|
4060
|
+
TaskCommand.refuse(parsed.reason);
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
const envFile = args["env-file"];
|
|
4064
|
+
if (envFile !== void 0) try {
|
|
4065
|
+
await TaskCommand.loadEnvironmentFile(resolve(process.cwd(), envFile));
|
|
4066
|
+
} catch (error) {
|
|
4067
|
+
TaskCommand.refuse(error instanceof Error ? error.message : String(error));
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
const sourceOption = TaskCommand.sourceOption(rawArgs);
|
|
4071
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, sourceOption);
|
|
4072
|
+
const exitCode = await new TaskRunner({
|
|
4073
|
+
rootDir,
|
|
4074
|
+
moduleName: args.module,
|
|
4075
|
+
taskName: args.task,
|
|
4076
|
+
withOutboxRelay: args["with-outbox-relay"],
|
|
4077
|
+
isSourceExplicit: sourceOption !== void 0 && sourceOption.trim() !== "",
|
|
4078
|
+
...parsed.payload === void 0 ? {} : { payload: parsed.payload },
|
|
4079
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
4080
|
+
}).run();
|
|
4081
|
+
TaskCommand.finish(exitCode);
|
|
4082
|
+
}
|
|
4083
|
+
});
|
|
4084
|
+
/** `heximon task` — run one-off work of the app. */
|
|
4085
|
+
const taskCommand = defineCommand({
|
|
4086
|
+
meta: {
|
|
4087
|
+
name: "task",
|
|
4088
|
+
description: "Run one-off work of the app."
|
|
4089
|
+
},
|
|
4090
|
+
subCommands: { run: taskRunCommand }
|
|
4091
|
+
});
|
|
4092
|
+
//#endregion
|
|
2458
4093
|
//#region src/workflow-status-report.ts
|
|
2459
4094
|
/** How one workflow's current step graph relates to the committed baseline gate. */
|
|
2460
4095
|
const WorkflowBaselineState = {
|
|
@@ -2915,7 +4550,8 @@ const main = defineCommand({
|
|
|
2915
4550
|
provision: provisionCommand,
|
|
2916
4551
|
migrations: migrationsCommand,
|
|
2917
4552
|
workflows: workflowsCommand,
|
|
2918
|
-
skills: skillsCommand
|
|
4553
|
+
skills: skillsCommand,
|
|
4554
|
+
task: taskCommand
|
|
2919
4555
|
}
|
|
2920
4556
|
});
|
|
2921
4557
|
await tab(main);
|