@orkestrel/scaffold 0.0.20 → 0.0.22
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/dist/bin/scaffold.js +385 -148
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/AGENTS.md +61 -42
- package/dist/host/CLAUDE.md +39 -387
- package/dist/host/agents/orchestration.md +454 -0
- package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +25 -7
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +42 -31
- package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +4 -4
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +12 -0
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +167 -0
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +98 -0
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +148 -0
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +4 -2
- package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +5 -5
- package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +2 -0
- package/dist/host/agents/skills/orkestrel-polish-surface/SKILL.md +11 -6
- package/dist/host/claude/agents/analyst.md +59 -0
- package/dist/host/claude/agents/application.md +1 -1
- package/dist/host/claude/agents/builder.md +1 -1
- package/dist/host/claude/agents/checker.md +5 -1
- package/dist/host/claude/agents/codex.md +120 -87
- package/dist/host/claude/agents/grok.md +44 -32
- package/dist/host/claude/agents/implementer.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +3 -3
- package/dist/host/claude/agents/planner.md +9 -3
- package/dist/host/claude/agents/researcher.md +6 -5
- package/dist/host/claude/agents/reviewer.md +17 -6
- package/dist/host/claude/agents/scout.md +7 -4
- package/dist/host/claude/agents/verifier.md +1 -1
- package/dist/host/claude/rules/application.md +17 -16
- package/dist/host/claude/rules/architecture.md +41 -22
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/names.md +7 -6
- package/dist/host/claude/rules/quality.md +71 -14
- package/dist/host/claude/rules/styles.md +1 -1
- package/dist/host/claude/rules/tests.md +18 -2
- package/dist/host/claude/rules/typescript.md +1 -1
- package/dist/host/claude/rules/workspace.md +6 -3
- package/dist/host/claude/settings.json +2 -0
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +10 -0
- package/dist/host/codex/agents/analyst.toml +15 -2
- package/dist/host/codex/agents/checker.toml +1 -1
- package/dist/host/codex/agents/grok.toml +2 -1
- package/dist/host/codex/agents/opus.toml +1 -1
- package/dist/host/codex/agents/planner.toml +1 -1
- package/dist/host/codex/agents/researcher.toml +2 -2
- package/dist/host/codex/agents/reviewer.toml +4 -2
- package/dist/host/codex/agents/scout.toml +2 -2
- package/dist/host/codex/config.toml +23 -64
- package/dist/host/cursor/rules/orchestration.mdc +33 -0
- package/dist/host/guides/src/scaffold.md +419 -150
- package/dist/host/manifest.json +47 -1
- package/dist/host/tests/setupPolicy.ts +130 -12
- package/dist/src/core/index.cjs +2059 -913
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -47
- package/dist/src/core/index.d.ts +202 -47
- package/dist/src/core/index.js +2050 -914
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +83 -33
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +32 -16
- package/dist/src/server/index.d.ts +32 -16
- package/dist/src/server/index.js +84 -35
- package/dist/src/server/index.js.map +1 -1
- package/package.json +7 -5
package/dist/src/core/index.cjs
CHANGED
|
@@ -103,20 +103,29 @@ var APP_MATRIX = Object.freeze({
|
|
|
103
103
|
* The byte-copied host artifact paths, frozen.
|
|
104
104
|
*
|
|
105
105
|
* @remarks
|
|
106
|
-
* The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`,
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* (`guides/src/
|
|
113
|
-
*
|
|
114
|
-
*
|
|
106
|
+
* The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`, the canonical
|
|
107
|
+
* orchestration contract (`.agents/orchestration.md`) every harness bridge
|
|
108
|
+
* points at, `.agents`, `.claude`, `.codex`, `.cursor`, the four SessionStart
|
|
109
|
+
* hook scripts (`scripts/deps.sh` / `scripts/cursor.sh` / `scripts/codex.sh` /
|
|
110
|
+
* `scripts/ollama.sh`), the repository coding-law policy module, the line's
|
|
111
|
+
* seven byte-identical root dotfiles, and the two guides-grouped mirror
|
|
112
|
+
* candidates: the line-wide dev-tooling guide (`guides/src/guide.md`) and the
|
|
113
|
+
* scaffold bin's own self-guide (`guides/src/scaffold.md`). `stageHost` vendors
|
|
114
|
+
* both; each plan carries the subset selected by `selectHostPaths`, omitting the
|
|
115
|
+
* target blueprint's own guide.
|
|
116
|
+
*
|
|
117
|
+
* Three harness bridges point at `.agents/orchestration.md` and carry only their
|
|
118
|
+
* own harness's specifics: `CLAUDE.md`, `.codex/config.toml`, and
|
|
119
|
+
* `.cursor/rules`. They are meaningless without the contract they reference, but
|
|
120
|
+
* they do not share a `Group` — `hostGroup` keeps `CLAUDE.md` in `docs` with the
|
|
121
|
+
* other root documents, so a plan selecting `orchestration` carries two of the
|
|
122
|
+
* three and a plan selecting `docs` carries the third.
|
|
115
123
|
*/
|
|
116
124
|
var HOST_PATHS = Object.freeze([
|
|
117
125
|
"AGENTS.md",
|
|
118
126
|
"CLAUDE.md",
|
|
119
127
|
"LICENSE",
|
|
128
|
+
".agents/orchestration.md",
|
|
120
129
|
".agents/skills",
|
|
121
130
|
".claude/agents",
|
|
122
131
|
".claude/rules",
|
|
@@ -125,6 +134,7 @@ var HOST_PATHS = Object.freeze([
|
|
|
125
134
|
".codex/agents",
|
|
126
135
|
".codex/config.toml",
|
|
127
136
|
".cursor/mcp.json",
|
|
137
|
+
".cursor/rules",
|
|
128
138
|
".mcp.json",
|
|
129
139
|
"scripts/deps.sh",
|
|
130
140
|
"scripts/cursor.sh",
|
|
@@ -141,12 +151,48 @@ var HOST_PATHS = Object.freeze([
|
|
|
141
151
|
"guides/src/guide.md",
|
|
142
152
|
"guides/src/scaffold.md"
|
|
143
153
|
]);
|
|
144
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* The path prefixes whose contents instruct or wire an agent, frozen.
|
|
156
|
+
*
|
|
157
|
+
* @remarks
|
|
158
|
+
* Group classification splits by what a path governs, not by where it sits:
|
|
159
|
+
* anything under these prefixes is `orchestration`, and everything else that is
|
|
160
|
+
* not source, tests, guides, docs, or a manifest is `configs`. Both classifiers
|
|
161
|
+
* — `inferGroup` for a foreign target path and `hostGroup` for a `HOST_PATHS`
|
|
162
|
+
* entry — read this one list, so a new harness directory is admitted once.
|
|
163
|
+
*/
|
|
164
|
+
var ORCHESTRATION_PATH_PREFIXES = Object.freeze([
|
|
165
|
+
".agents/",
|
|
166
|
+
".claude/",
|
|
167
|
+
".codex/",
|
|
168
|
+
".cursor/",
|
|
169
|
+
".github/",
|
|
170
|
+
"scripts/"
|
|
171
|
+
]);
|
|
172
|
+
/**
|
|
173
|
+
* The exact root filenames that wire an agent bench rather than the toolchain, frozen.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* `.mcp.json` registers MCP servers for the harness. It sits among the root
|
|
177
|
+
* dotfiles but governs agents, so it groups with the harness bridges.
|
|
178
|
+
*/
|
|
179
|
+
var ORCHESTRATION_PATH_NAMES = Object.freeze([".mcp.json"]);
|
|
180
|
+
/** The birth-only provisioner skeleton retained by workspaces with declared service vendors. */
|
|
145
181
|
var SERVICE_SCRIPT_PATH = "scripts/service.sh";
|
|
146
182
|
/** The consumer-owned Vitest global-setup module shared by its independently selected projects. */
|
|
147
183
|
var GLOBAL_SETUP_PATH = "tests/setupGlobal.ts";
|
|
148
184
|
/** The consumer-owned Vite wrapper whose physical presence enables the optional app showcase. */
|
|
149
185
|
var SHOWCASE_CONFIG_PATH = "configs/app/vite.showcase.config.ts";
|
|
186
|
+
/**
|
|
187
|
+
* The catalog agent file whose bounded marker region the catalog operation alone owns.
|
|
188
|
+
*
|
|
189
|
+
* @remarks
|
|
190
|
+
* Vendored like every other host artifact, but presence-owned after hydration:
|
|
191
|
+
* `diffPlan` compares this one path by presence, so a consumer restores it while
|
|
192
|
+
* absent and never replaces its bytes — not from an audit, not from a repair, and
|
|
193
|
+
* not under `replace`.
|
|
194
|
+
*/
|
|
195
|
+
var CATALOG_AGENT_PATH = ".claude/agents/orkestrel.md";
|
|
150
196
|
/** The package-name RegExp — lowercase alphanumeric-with-hyphens, letter-first. */
|
|
151
197
|
var NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
152
198
|
/** Maximum bare workspace name length beneath the generated `@orkestrel/` scope. */
|
|
@@ -229,15 +275,15 @@ var DEFAULT_VERSION = "0.0.1";
|
|
|
229
275
|
/** The `engines.node` range the `blueprint` builder fills. */
|
|
230
276
|
var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
|
|
231
277
|
/** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
|
|
232
|
-
var SCAFFOLD_RANGE = "^0.0.
|
|
278
|
+
var SCAFFOLD_RANGE = "^0.0.22";
|
|
233
279
|
/** Tooling versions shared by scaffold and every generated workspace. */
|
|
234
280
|
var BASE_DEV_DEPENDENCIES = Object.freeze({
|
|
235
281
|
"@microsoft/api-extractor": "^7.58.12",
|
|
236
282
|
"@orkestrel/guide": "^0.0.8",
|
|
237
283
|
"@orkestrel/scaffold": SCAFFOLD_RANGE,
|
|
238
284
|
"@types/node": "^26.1.2",
|
|
239
|
-
oxfmt: "^0.
|
|
240
|
-
oxlint: "^1.
|
|
285
|
+
oxfmt: "^0.62.0",
|
|
286
|
+
oxlint: "^1.77.0",
|
|
241
287
|
typescript: "^6.0.3",
|
|
242
288
|
vite: "~8.2.0",
|
|
243
289
|
"vite-plugin-dts": "^5.0.3",
|
|
@@ -248,13 +294,23 @@ var SOURCE_BROWSER_DEV_DEPENDENCIES = Object.freeze({
|
|
|
248
294
|
"@vitest/browser-playwright": "^4.1.10",
|
|
249
295
|
playwright: "^1.62.1"
|
|
250
296
|
});
|
|
297
|
+
/** Baseline development dependency required by every private application environment. */
|
|
298
|
+
var APP_DEV_DEPENDENCIES = Object.freeze({ "@orkestrel/contract": "^0.0.9" });
|
|
251
299
|
/** Additional development dependencies required by a private Vue browser application. */
|
|
252
300
|
var APP_BROWSER_DEV_DEPENDENCIES = Object.freeze({
|
|
253
301
|
...SOURCE_BROWSER_DEV_DEPENDENCIES,
|
|
302
|
+
"@orkestrel/html": "^0.0.2",
|
|
254
303
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
255
304
|
vue: "^3.5.40",
|
|
256
305
|
"vue-tsc": "^3.3.7"
|
|
257
306
|
});
|
|
307
|
+
/** Additional development dependencies required by a private server application. */
|
|
308
|
+
var APP_SERVER_DEV_DEPENDENCIES = Object.freeze({
|
|
309
|
+
"@orkestrel/emitter": "^0.0.5",
|
|
310
|
+
"@orkestrel/middleware": "^0.0.9",
|
|
311
|
+
"@orkestrel/router": "^0.0.8",
|
|
312
|
+
"@orkestrel/server": "^0.0.10"
|
|
313
|
+
});
|
|
258
314
|
/** Immutable official actions/checkout v6.0.2 commit used by generated CI. */
|
|
259
315
|
var CHECKOUT_ACTION_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
|
|
260
316
|
/** Immutable official actions/setup-node v6.4.0 commit used by generated CI. */
|
|
@@ -428,8 +484,8 @@ function member(name, category, summary, environment = "core") {
|
|
|
428
484
|
* @remarks
|
|
429
485
|
* `version` / `engines` default `DEFAULT_VERSION` / `DEFAULT_ENGINES`,
|
|
430
486
|
* `src` defaults `['core']`, and `app` / `keywords` / `dependencies` /
|
|
431
|
-
* `peers` / `extras` / `overrides` default `[]`, and `bin` /
|
|
432
|
-
* `
|
|
487
|
+
* `peers` / `extras` / `overrides` / `services` default `[]`, and `bin` /
|
|
488
|
+
* `integration` / `global` / `showcase` default `false`. `description` is OMITTED entirely
|
|
433
489
|
* when absent, so the result round-trips the exact-record `Blueprint` guard.
|
|
434
490
|
* @returns A complete `Blueprint`.
|
|
435
491
|
*
|
|
@@ -454,7 +510,7 @@ function blueprint(name, options) {
|
|
|
454
510
|
overrides: options?.overrides ?? [],
|
|
455
511
|
bin: options?.bin ?? false,
|
|
456
512
|
integration: options?.integration ?? false,
|
|
457
|
-
|
|
513
|
+
services: [...options?.services ?? []].sort(),
|
|
458
514
|
global: options?.global ?? false,
|
|
459
515
|
showcase: options?.showcase ?? false
|
|
460
516
|
};
|
|
@@ -528,13 +584,45 @@ function serializeTypeScriptString(value) {
|
|
|
528
584
|
return `${output}'`;
|
|
529
585
|
}
|
|
530
586
|
/**
|
|
587
|
+
* Determine whether an application blueprint spans the shared browser/server boundary.
|
|
588
|
+
*
|
|
589
|
+
* @param spec - The blueprint to inspect.
|
|
590
|
+
* @returns True only when app/core, app/browser, and app/server are all selected.
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```ts
|
|
594
|
+
* hasApplicationBoundary(blueprint('application', { app: ['core', 'browser', 'server'] }))
|
|
595
|
+
* ```
|
|
596
|
+
*/
|
|
597
|
+
function hasApplicationBoundary(spec) {
|
|
598
|
+
return spec.app.includes("core") && spec.app.includes("browser") && spec.app.includes("server");
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Determine whether an application blueprint emits its browser showcase.
|
|
602
|
+
*
|
|
603
|
+
* @param spec - The blueprint to inspect.
|
|
604
|
+
* @returns True only when showcase intent accompanies app/browser.
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```ts
|
|
608
|
+
* hasApplicationShowcase(blueprint('application', { app: ['browser'], showcase: true }))
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
function hasApplicationShowcase(spec) {
|
|
612
|
+
return spec.showcase && spec.app.includes("browser");
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
531
615
|
* Derive the declared public `Member[]` from a blueprint.
|
|
532
616
|
*
|
|
533
617
|
* @param spec - The blueprint to derive members from.
|
|
534
618
|
* @remarks
|
|
535
619
|
* Published source environments receive the canonical entity/type/factory/constant
|
|
536
620
|
* inventory. Application environments receive their exact public declaration kinds,
|
|
537
|
-
* including parsers, guards, handlers, errors, and runners where present.
|
|
621
|
+
* including parsers, guards, handlers, errors, and runners where present. Two groups
|
|
622
|
+
* move rather than duplicate: the health contract is declared against `app/server`
|
|
623
|
+
* while the server alone reads it and against `app/core` once the browser reads it
|
|
624
|
+
* too, and the showcase seed, factory, and root-view identity appear only for a
|
|
625
|
+
* blueprint whose showcase accompanies `app/browser`.
|
|
538
626
|
* @returns The declared `Member[]`, one set per environment.
|
|
539
627
|
*
|
|
540
628
|
* @example
|
|
@@ -547,6 +635,8 @@ function serializeTypeScriptString(value) {
|
|
|
547
635
|
function blueprintToMembers(spec) {
|
|
548
636
|
const pascal = pascalCase(spec.name);
|
|
549
637
|
const members = [];
|
|
638
|
+
const hasBoundary = hasApplicationBoundary(spec);
|
|
639
|
+
const hasShowcase = hasApplicationShowcase(spec);
|
|
550
640
|
for (const environment of spec.src) {
|
|
551
641
|
members.push(member(pascal, "entity", `The ${pascal} entity.`, environment));
|
|
552
642
|
members.push(member(`${pascal}Options`, "type", `Options for creating a ${pascal}.`, environment));
|
|
@@ -564,15 +654,21 @@ function blueprintToMembers(spec) {
|
|
|
564
654
|
members.push(member("isApplicationError", "guard", "Narrow a caught value to ApplicationError.", "core"));
|
|
565
655
|
members.push(member("parseApplicationName", "parser", "Parse an application name.", "core"));
|
|
566
656
|
members.push(member("createApplication", "factory", "Create an application identity.", "core"));
|
|
657
|
+
if (hasBoundary) members.push(member("ApplicationRecord", "type", "The shared application health record.", "core"), member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "core"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "core"), member("APP_HEALTH_TIMEOUT", "constant", "The shared health read timeout.", "core"), member("isApplicationRecord", "guard", "Narrow a transport value to the shared record.", "core"), member("readApplicationHealth", "handler", "Read the shared health boundary as the application identity.", "core"));
|
|
567
658
|
}
|
|
568
659
|
if (spec.app.includes("browser")) {
|
|
569
660
|
members.push(member("BrowserApplicationErrorCode", "alias", "A browser application configuration error reason.", "browser"), member("BrowserApplicationErrorContext", "type", "Browser application boundary-failure context.", "browser"), member("BrowserApplicationOptions", "type", "Options for creating the browser application.", "browser"));
|
|
570
661
|
members.push(member("MAX_BROWSER_APPLICATION_NAME_LENGTH", "constant", "The maximum browser application-name length.", "browser"), member("MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH", "constant", "The maximum raw browser application-name input length.", "browser"), member("BrowserApplicationError", "error", "A browser application configuration error.", "browser"), member("isBrowserApplicationError", "guard", "Narrow a caught value to BrowserApplicationError.", "browser"), member("parseBrowserApplicationOptions", "parser", "Parse browser application options.", "browser"));
|
|
571
662
|
if (!spec.app.includes("core")) members.push(member("APP_NAME", "constant", "The browser application name.", "browser"));
|
|
663
|
+
if (hasShowcase && !spec.app.includes("core")) members.push(member("Application", "type", "The identity the root view renders.", "browser"));
|
|
664
|
+
if (hasShowcase) members.push(member("seedApplication", "factory", "Seed the inert showcase identity.", "browser"));
|
|
572
665
|
members.push(member("createBrowserApplication", "factory", "Create an unmounted Vue application.", "browser"));
|
|
666
|
+
if (hasShowcase) members.push(member("mountShowcaseApplication", "factory", "Mount the seeded showcase.", "browser"));
|
|
667
|
+
if (hasBoundary) members.push(member("mountBrowserApplication", "factory", "Mount the application over its server boundary.", "browser"));
|
|
573
668
|
}
|
|
574
669
|
if (spec.app.includes("server")) {
|
|
575
|
-
|
|
670
|
+
if (!hasBoundary) members.push(member("ApplicationRecord", "type", "The application health record.", "server"));
|
|
671
|
+
members.push(member("ApplicationState", "type", "Per-request application state.", "server"), member("ApplicationServerErrorCode", "alias", "An application server error reason.", "server"), member("ApplicationServerErrorContext", "type", "Application server boundary-failure context.", "server"), member("ApplicationServerOptions", "type", "Options for creating an application server.", "server"), member("ApplicationServerInterface", "type", "The application server lifecycle contract.", "server"), member("ApplicationServerRunnerInterface", "type", "The application server process lifecycle contract.", "server"), member("ApplicationServerRunnerEventMap", "alias", "Observable application server runner outcomes.", "server"), member("ApplicationServerRunnerOptions", "type", "Options for observing an application server runner.", "server"), member("DEFAULT_APP_HOST", "constant", "The loopback host default.", "server"), member("DEFAULT_APP_PORT", "constant", "The application port default.", "server"), member("DEFAULT_APP_START_TIMEOUT", "constant", "The application startup timeout default.", "server"), member("MAX_APP_START_TIMEOUT", "constant", "The maximum application startup timeout.", "server"), member("MAX_APP_HOST_INPUT_LENGTH", "constant", "The maximum raw application-host input length.", "server"), member("MAX_APP_NUMBER_INPUT_LENGTH", "constant", "The maximum raw application numeric input length.", "server"), member("APP_PORT_PATTERN", "constant", "The decimal application-port syntax.", "server"), member("APP_HOST_LABEL_PATTERN", "constant", "The DNS application-host label syntax.", "server"), member("APP_NUMERIC_HOST_PATTERN", "constant", "The ambiguous numeric-host rejection syntax.", "server"), ...hasBoundary ? [] : [member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "server"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "server")], member("createApplicationDispatcher", "factory", "Create a standalone application route dispatcher.", "server"), member("ApplicationServer", "entity", "The composed application server.", "server"), member("ApplicationServerRunner", "entity", "The application server process lifecycle owner.", "server"), member("ApplicationServerError", "error", "A server configuration or lifecycle error.", "server"), member("isApplicationServerError", "guard", "Narrow a caught value to ApplicationServerError.", "server"), member("parseApplicationHost", "parser", "Parse an application host.", "server"), member("parseApplicationPort", "parser", "Parse an application port.", "server"), member("parseApplicationStartTimeout", "parser", "Parse an application startup timeout.", "server"), member("parseApplicationServerOptions", "parser", "Parse application server options.", "server"), member("handleApplicationHealth", "handler", "Return the application health record.", "server"), member("reportApplicationServerError", "handler", "Report a process-owned failure without exposing diagnostic context.", "server"), member("createApplicationServer", "factory", "Create a stopped application server.", "server"), member("startApplicationServer", "factory", "Start the process-owned application server.", "server"));
|
|
576
672
|
if (!spec.app.includes("core")) members.push(member("APP_NAME", "constant", "The server application name.", "server"));
|
|
577
673
|
}
|
|
578
674
|
return members;
|
|
@@ -903,7 +999,7 @@ function syncToReview(report) {
|
|
|
903
999
|
}
|
|
904
1000
|
/**
|
|
905
1001
|
* Project a fleet package catalog into a markdown table — the block
|
|
906
|
-
*
|
|
1002
|
+
* `CATALOG_AGENT_PATH`'s catalog markers wrap.
|
|
907
1003
|
*
|
|
908
1004
|
* @param entries - The catalog rows to render.
|
|
909
1005
|
* @remarks
|
|
@@ -935,10 +1031,12 @@ function catalogToBlock(entries) {
|
|
|
935
1031
|
*
|
|
936
1032
|
* @param path - The target-relative path to classify.
|
|
937
1033
|
* @remarks
|
|
938
|
-
* Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`,
|
|
939
|
-
*
|
|
940
|
-
* `
|
|
941
|
-
*
|
|
1034
|
+
* Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`, then
|
|
1035
|
+
* `matchesOrchestrationPath`, which owns the orchestration membership rule for
|
|
1036
|
+
* this function and for `hostGroup` alike, then the two manifest files by exact
|
|
1037
|
+
* name. Anything left falls through to `'configs'`. Read the rule at
|
|
1038
|
+
* `matchesOrchestrationPath` rather than here; one rule with two descriptions
|
|
1039
|
+
* drifts exactly as fast as one rule with two implementations.
|
|
942
1040
|
* @returns The inferred `Group` for `path`.
|
|
943
1041
|
*
|
|
944
1042
|
* @example
|
|
@@ -956,23 +1054,43 @@ function inferGroup(path) {
|
|
|
956
1054
|
if (path.startsWith("guides/")) return "guides";
|
|
957
1055
|
if (path.startsWith("docs/")) return "docs";
|
|
958
1056
|
if (path.startsWith("configs/")) return "configs";
|
|
959
|
-
if (
|
|
1057
|
+
if (matchesOrchestrationPath(path)) return "orchestration";
|
|
960
1058
|
if (path === "package.json" || path === "package-lock.json") return "manifest";
|
|
961
1059
|
return "configs";
|
|
962
1060
|
}
|
|
963
1061
|
/**
|
|
1062
|
+
* Test whether a path instructs or wires an agent rather than the toolchain.
|
|
1063
|
+
*
|
|
1064
|
+
* @param path - The portable path to classify.
|
|
1065
|
+
* @returns `true` when the path is agent orchestration.
|
|
1066
|
+
*
|
|
1067
|
+
* @example
|
|
1068
|
+
* ```ts
|
|
1069
|
+
* import { matchesOrchestrationPath } from '@orkestrel/scaffold'
|
|
1070
|
+
*
|
|
1071
|
+
* matchesOrchestrationPath('.cursor/rules/orchestration.mdc') // true
|
|
1072
|
+
* matchesOrchestrationPath('.mcp.json') // true
|
|
1073
|
+
* matchesOrchestrationPath('.oxlintrc.json') // false
|
|
1074
|
+
* ```
|
|
1075
|
+
*/
|
|
1076
|
+
function matchesOrchestrationPath(path) {
|
|
1077
|
+
if (ORCHESTRATION_PATH_NAMES.includes(path)) return true;
|
|
1078
|
+
return ORCHESTRATION_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
964
1081
|
* Diff a plan's artifacts against a target's current content.
|
|
965
1082
|
*
|
|
966
1083
|
* @param plan - The plan whose artifacts are the source of truth.
|
|
967
1084
|
* @param current - The target's current content, keyed by artifact-relative path.
|
|
968
1085
|
* @remarks
|
|
969
|
-
* Audit semantics are per-origin. A `host`-origin artifact
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
1086
|
+
* Audit semantics are per-origin. A `host`-origin artifact with canonical
|
|
1087
|
+
* `hex` is content-compared exactly like a `computed` artifact and CAN be
|
|
1088
|
+
* `stale`. A host artifact without canonical bytes is presence-owned:
|
|
1089
|
+
* present is `aligned`, absent is `missing`. `CATALOG_AGENT_PATH` remains
|
|
1090
|
+
* presence-owned after hydration because the catalog operation alone owns its
|
|
1091
|
+
* bounded marker region. `hydratePlan` expands directory-shaped host artifacts
|
|
1092
|
+
* into byte-aware file artifacts; only an intentional dependency-guide pointer
|
|
1093
|
+
* may remain without `hex`. A `computed` artifact is content-aware canon —
|
|
976
1094
|
* `missing` / `aligned` / `stale` — and gates the audit like any drifted
|
|
977
1095
|
* finding. A `template`-origin artifact is BIRTH-ONLY and AUDIT-EXEMPT: it is
|
|
978
1096
|
* always reported `aligned`, regardless of whether the target has it at all
|
|
@@ -1012,7 +1130,7 @@ function diffPlan(plan, current) {
|
|
|
1012
1130
|
if (artifact.origin === "host") {
|
|
1013
1131
|
let drift;
|
|
1014
1132
|
if (seen === void 0) drift = "missing";
|
|
1015
|
-
else if (artifact.hex === void 0) drift = "aligned";
|
|
1133
|
+
else if (artifact.path === ".claude/agents/orkestrel.md" || artifact.hex === void 0) drift = "aligned";
|
|
1016
1134
|
else drift = seen === artifact.hex ? "aligned" : "stale";
|
|
1017
1135
|
findings.push({
|
|
1018
1136
|
path: artifact.path,
|
|
@@ -1369,6 +1487,35 @@ function validateBlueprint(spec) {
|
|
|
1369
1487
|
text: "Showcase requires the app browser environment",
|
|
1370
1488
|
blocking: true
|
|
1371
1489
|
});
|
|
1490
|
+
const seenServices = /* @__PURE__ */ new Set();
|
|
1491
|
+
const serviceProjects = /* @__PURE__ */ new Set();
|
|
1492
|
+
let previousService;
|
|
1493
|
+
for (const service of spec.services) {
|
|
1494
|
+
if (!NAME_PATTERN.test(service) || service.length > 203) questions.push({
|
|
1495
|
+
field: "services",
|
|
1496
|
+
text: `Service name "${service}" must be a bounded lowercase directory name matching ${NAME_PATTERN.source}`,
|
|
1497
|
+
blocking: true
|
|
1498
|
+
});
|
|
1499
|
+
if (seenServices.has(service)) questions.push({
|
|
1500
|
+
field: "services",
|
|
1501
|
+
text: `Service "${service}" is declared more than once`,
|
|
1502
|
+
blocking: true
|
|
1503
|
+
});
|
|
1504
|
+
if (previousService !== void 0 && previousService > service) questions.push({
|
|
1505
|
+
field: "services",
|
|
1506
|
+
text: "Services must be sorted by directory name",
|
|
1507
|
+
blocking: true
|
|
1508
|
+
});
|
|
1509
|
+
const project = pascalCase(service);
|
|
1510
|
+
if (serviceProjects.has(project)) questions.push({
|
|
1511
|
+
field: "services",
|
|
1512
|
+
text: `Service "${service}" collides with another generated project name`,
|
|
1513
|
+
blocking: true
|
|
1514
|
+
});
|
|
1515
|
+
seenServices.add(service);
|
|
1516
|
+
serviceProjects.add(project);
|
|
1517
|
+
previousService = service;
|
|
1518
|
+
}
|
|
1372
1519
|
if (spec.src.length > 0) {
|
|
1373
1520
|
for (const environment of spec.src) if (!ENVIRONMENTS.includes(environment)) questions.push({
|
|
1374
1521
|
field: "src",
|
|
@@ -1653,6 +1800,34 @@ function renderArray(entries, indent, prefix, suffix) {
|
|
|
1653
1800
|
return `[\n${items.map((item) => `${childIndent}${item}`).join(",\n")}\n${indent}]`;
|
|
1654
1801
|
}
|
|
1655
1802
|
/**
|
|
1803
|
+
* Render a single-quoted TypeScript string array literal through `oxfmt`'s
|
|
1804
|
+
* inline-or-broken rule — inline when the rendered width fits
|
|
1805
|
+
* `JSON_PRINT_WIDTH`, one item per line with a trailing comma on every line
|
|
1806
|
+
* (including the last) otherwise, matching `.oxfmtrc.json`'s
|
|
1807
|
+
* `trailingComma: "all"` for non-JSON files.
|
|
1808
|
+
*
|
|
1809
|
+
* @param entries - The array's string elements, in order.
|
|
1810
|
+
* @param indent - The current indentation prefix.
|
|
1811
|
+
* @param prefix - The text already emitted on this line before the array.
|
|
1812
|
+
* @param suffix - The text that will follow the array on this line.
|
|
1813
|
+
* @returns The rendered array fragment (no trailing newline).
|
|
1814
|
+
*
|
|
1815
|
+
* @example
|
|
1816
|
+
* ```ts
|
|
1817
|
+
* import { renderStringArray } from '@orkestrel/scaffold'
|
|
1818
|
+
*
|
|
1819
|
+
* renderStringArray(['app', 'guides', 'tests'], '', '', '') // "['app', 'guides', 'tests']"
|
|
1820
|
+
* ```
|
|
1821
|
+
*/
|
|
1822
|
+
function renderStringArray(entries, indent, prefix, suffix) {
|
|
1823
|
+
if (entries.length === 0) return "[]";
|
|
1824
|
+
const items = entries.map((entry) => serializeTypeScriptString(entry));
|
|
1825
|
+
const inline = `[${items.join(", ")}]`;
|
|
1826
|
+
if (fitsPrintWidth(`${prefix}${inline}${suffix}`)) return inline;
|
|
1827
|
+
const childIndent = `${indent}\t`;
|
|
1828
|
+
return `[\n${items.map((item) => `${childIndent}${item},`).join("\n")}\n${indent}]`;
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1656
1831
|
* Render a JSON object through `formatJson`'s one-key-per-line rule.
|
|
1657
1832
|
*
|
|
1658
1833
|
* @param entry - The object to render.
|
|
@@ -1831,7 +2006,10 @@ function blueprintShape() {
|
|
|
1831
2006
|
overrides: (0, _orkestrel_contract.arrayShape)(overrideShape(), { max: MAX_COLLECTION_ITEMS }),
|
|
1832
2007
|
bin: (0, _orkestrel_contract.booleanShape)(),
|
|
1833
2008
|
integration: (0, _orkestrel_contract.booleanShape)(),
|
|
1834
|
-
|
|
2009
|
+
services: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)({
|
|
2010
|
+
min: 1,
|
|
2011
|
+
max: 203
|
|
2012
|
+
}), { max: MAX_COLLECTION_ITEMS }),
|
|
1835
2013
|
global: (0, _orkestrel_contract.booleanShape)(),
|
|
1836
2014
|
showcase: (0, _orkestrel_contract.booleanShape)()
|
|
1837
2015
|
});
|
|
@@ -2895,7 +3073,10 @@ export * from './factories.js'
|
|
|
2895
3073
|
name: "appCoreTypes",
|
|
2896
3074
|
summary: "The host-independent application contract.",
|
|
2897
3075
|
category: "source",
|
|
2898
|
-
placeholders: Object.freeze([
|
|
3076
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3077
|
+
name: "record",
|
|
3078
|
+
description: "The shared health record declared once both hosts read it."
|
|
3079
|
+
})]),
|
|
2899
3080
|
content: `/** A rejected shared application boundary. */
|
|
2900
3081
|
${EXPORT_KEYWORD} type ApplicationErrorCode = 'CONFIG'
|
|
2901
3082
|
|
|
@@ -2909,7 +3090,7 @@ ${EXPORT_KEYWORD} interface ApplicationErrorContext {
|
|
|
2909
3090
|
${EXPORT_KEYWORD} interface Application {
|
|
2910
3091
|
readonly name: string
|
|
2911
3092
|
}
|
|
2912
|
-
`
|
|
3093
|
+
{{record}}`
|
|
2913
3094
|
}),
|
|
2914
3095
|
appCoreConstants: Object.freeze({
|
|
2915
3096
|
id: "appCoreConstants",
|
|
@@ -2919,6 +3100,9 @@ ${EXPORT_KEYWORD} interface Application {
|
|
|
2919
3100
|
placeholders: Object.freeze([Object.freeze({
|
|
2920
3101
|
name: "nameLiteral",
|
|
2921
3102
|
description: "The JSON-serialized application name."
|
|
3103
|
+
}), Object.freeze({
|
|
3104
|
+
name: "health",
|
|
3105
|
+
description: "The shared health route constants declared once both hosts read them."
|
|
2922
3106
|
})]),
|
|
2923
3107
|
content: `/** The application name shared by every host environment. */
|
|
2924
3108
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = {{nameLiteral}}
|
|
@@ -2928,7 +3112,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_LENGTH = 203
|
|
|
2928
3112
|
|
|
2929
3113
|
/** Maximum raw Unicode code units inspected before trimming an application name. */
|
|
2930
3114
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_INPUT_LENGTH = 255
|
|
2931
|
-
`
|
|
3115
|
+
{{health}}`
|
|
2932
3116
|
}),
|
|
2933
3117
|
appCoreErrors: Object.freeze({
|
|
2934
3118
|
id: "appCoreErrors",
|
|
@@ -2937,6 +3121,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_INPUT_LENGTH = 255
|
|
|
2937
3121
|
category: "source",
|
|
2938
3122
|
placeholders: Object.freeze([]),
|
|
2939
3123
|
content: `import type { ApplicationErrorCode, ApplicationErrorContext } from './types.js'
|
|
3124
|
+
import { holds } from '@orkestrel/contract'
|
|
2940
3125
|
|
|
2941
3126
|
/** A rejected shared application configuration value. */
|
|
2942
3127
|
${EXPORT_KEYWORD} class ApplicationError extends Error {
|
|
@@ -2965,11 +3150,34 @@ ${EXPORT_KEYWORD} class ApplicationError extends Error {
|
|
|
2965
3150
|
* \`\`\`
|
|
2966
3151
|
*/
|
|
2967
3152
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationError(value: unknown): value is ApplicationError {
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3153
|
+
return holds(() => value instanceof ApplicationError)
|
|
3154
|
+
}
|
|
3155
|
+
`
|
|
3156
|
+
}),
|
|
3157
|
+
appCoreValidators: Object.freeze({
|
|
3158
|
+
id: "appCoreValidators",
|
|
3159
|
+
name: "appCoreValidators",
|
|
3160
|
+
summary: "The host-independent guard over the shared health record.",
|
|
3161
|
+
category: "source",
|
|
3162
|
+
placeholders: Object.freeze([]),
|
|
3163
|
+
content: `import type { ApplicationRecord } from './types.js'
|
|
3164
|
+
import { holds, isNonEmptyString, isRecord } from '@orkestrel/contract'
|
|
3165
|
+
|
|
3166
|
+
/**
|
|
3167
|
+
* Narrow one unvalidated transport value to the shared application record.
|
|
3168
|
+
*
|
|
3169
|
+
* @param value - The value read from the health route, before validation.
|
|
3170
|
+
* @returns True only for the exact record both hosts agreed on.
|
|
3171
|
+
*
|
|
3172
|
+
* @example
|
|
3173
|
+
* \`\`\`ts
|
|
3174
|
+
* import { isApplicationRecord } from '@app/core'
|
|
3175
|
+
*
|
|
3176
|
+
* isApplicationRecord({ name: 'example', status: 'ok' }) // true
|
|
3177
|
+
* \`\`\`
|
|
3178
|
+
*/
|
|
3179
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationRecord(value: unknown): value is ApplicationRecord {
|
|
3180
|
+
return holds(() => isRecord(value) && isNonEmptyString(value.name) && value.status === 'ok')
|
|
2973
3181
|
}
|
|
2974
3182
|
`
|
|
2975
3183
|
}),
|
|
@@ -2979,7 +3187,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationError(value: unknown): value
|
|
|
2979
3187
|
summary: "The host-independent application value parsers.",
|
|
2980
3188
|
category: "source",
|
|
2981
3189
|
placeholders: Object.freeze([]),
|
|
2982
|
-
content: `import {
|
|
3190
|
+
content: `import { isNonEmptyString } from '@orkestrel/contract'
|
|
3191
|
+
import { MAX_APPLICATION_NAME_INPUT_LENGTH, MAX_APPLICATION_NAME_LENGTH } from './constants.js'
|
|
2983
3192
|
import { ApplicationError } from './errors.js'
|
|
2984
3193
|
|
|
2985
3194
|
/**
|
|
@@ -3001,7 +3210,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationName(value: unknown): stri
|
|
|
3001
3210
|
throw new ApplicationError('CONFIG', 'Application name must be a string', { value })
|
|
3002
3211
|
}
|
|
3003
3212
|
const name = value.trim()
|
|
3004
|
-
if (name
|
|
3213
|
+
if (!isNonEmptyString(name) || name.length > MAX_APPLICATION_NAME_LENGTH) {
|
|
3005
3214
|
throw new ApplicationError(
|
|
3006
3215
|
'CONFIG',
|
|
3007
3216
|
\`Application name must contain 1 through \${MAX_APPLICATION_NAME_LENGTH} characters\`,
|
|
@@ -3010,6 +3219,52 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationName(value: unknown): stri
|
|
|
3010
3219
|
}
|
|
3011
3220
|
return name
|
|
3012
3221
|
}
|
|
3222
|
+
`
|
|
3223
|
+
}),
|
|
3224
|
+
appCoreHandlers: Object.freeze({
|
|
3225
|
+
id: "appCoreHandlers",
|
|
3226
|
+
name: "appCoreHandlers",
|
|
3227
|
+
summary: "The host-independent read of the shared health boundary.",
|
|
3228
|
+
category: "source",
|
|
3229
|
+
placeholders: Object.freeze([]),
|
|
3230
|
+
content: `import type { Application } from './types.js'
|
|
3231
|
+
import { APP_HEALTH_PATH, APP_HEALTH_TIMEOUT } from './constants.js'
|
|
3232
|
+
import { isApplicationRecord } from './validators.js'
|
|
3233
|
+
|
|
3234
|
+
/**
|
|
3235
|
+
* Read the application server's health route and translate its unvalidated JSON
|
|
3236
|
+
* into the shared application identity.
|
|
3237
|
+
*
|
|
3238
|
+
* @param origin - The absolute origin serving the application health route.
|
|
3239
|
+
* @returns The identity the running server reported, or undefined when the boundary
|
|
3240
|
+
* is unreachable, too slow, or off-contract.
|
|
3241
|
+
*
|
|
3242
|
+
* @remarks
|
|
3243
|
+
* The single translation point between the two hosts: the response body is read as
|
|
3244
|
+
* \`unknown\` and narrowed by {@link isApplicationRecord} before any field is consumed,
|
|
3245
|
+
* so a missing, slow, or foreign server degrades to \`undefined\` instead of leaking an
|
|
3246
|
+
* unvalidated value into the application. The record's \`status\` proves liveness; the
|
|
3247
|
+
* identity is what the caller renders.
|
|
3248
|
+
*
|
|
3249
|
+
* @example
|
|
3250
|
+
* \`\`\`ts
|
|
3251
|
+
* import { readApplicationHealth } from '@app/core'
|
|
3252
|
+
*
|
|
3253
|
+
* await readApplicationHealth('http://127.0.0.1:3000') // { name: 'example' }
|
|
3254
|
+
* \`\`\`
|
|
3255
|
+
*/
|
|
3256
|
+
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} readApplicationHealth(origin: string): Promise<Application | undefined> {
|
|
3257
|
+
try {
|
|
3258
|
+
const response = await fetch(new URL(APP_HEALTH_PATH, origin), {
|
|
3259
|
+
signal: AbortSignal.timeout(APP_HEALTH_TIMEOUT),
|
|
3260
|
+
})
|
|
3261
|
+
if (!response.ok) return undefined
|
|
3262
|
+
const record: unknown = await response.json()
|
|
3263
|
+
return isApplicationRecord(record) ? Object.freeze({ name: record.name }) : undefined
|
|
3264
|
+
} catch {
|
|
3265
|
+
return undefined
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3013
3268
|
`
|
|
3014
3269
|
}),
|
|
3015
3270
|
appCoreFactories: Object.freeze({
|
|
@@ -3045,12 +3300,18 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplication(name: string = APP_NAME)
|
|
|
3045
3300
|
name: "appCoreIndex",
|
|
3046
3301
|
summary: "The application core barrel.",
|
|
3047
3302
|
category: "source",
|
|
3048
|
-
placeholders: Object.freeze([
|
|
3303
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3304
|
+
name: "validators",
|
|
3305
|
+
description: "The optional shared-record guard barrel row."
|
|
3306
|
+
}), Object.freeze({
|
|
3307
|
+
name: "handlers",
|
|
3308
|
+
description: "The optional shared health-boundary barrel row."
|
|
3309
|
+
})]),
|
|
3049
3310
|
content: `export * from './types.js'
|
|
3050
3311
|
export * from './constants.js'
|
|
3051
3312
|
export * from './errors.js'
|
|
3052
|
-
export * from './parsers.js'
|
|
3053
|
-
export * from './factories.js'
|
|
3313
|
+
{{validators}}export * from './parsers.js'
|
|
3314
|
+
{{handlers}}export * from './factories.js'
|
|
3054
3315
|
`
|
|
3055
3316
|
}),
|
|
3056
3317
|
appBrowserTypes: Object.freeze({
|
|
@@ -3058,7 +3319,10 @@ export * from './factories.js'
|
|
|
3058
3319
|
name: "appBrowserTypes",
|
|
3059
3320
|
summary: "The browser application options.",
|
|
3060
3321
|
category: "source",
|
|
3061
|
-
placeholders: Object.freeze([
|
|
3322
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3323
|
+
name: "application",
|
|
3324
|
+
description: "The browser-owned root-view identity declared without application core."
|
|
3325
|
+
})]),
|
|
3062
3326
|
content: `/** A rejected browser application boundary. */
|
|
3063
3327
|
${EXPORT_KEYWORD} type BrowserApplicationErrorCode = 'CONFIG'
|
|
3064
3328
|
|
|
@@ -3072,7 +3336,7 @@ ${EXPORT_KEYWORD} interface BrowserApplicationErrorContext {
|
|
|
3072
3336
|
${EXPORT_KEYWORD} interface BrowserApplicationOptions {
|
|
3073
3337
|
readonly name?: string
|
|
3074
3338
|
}
|
|
3075
|
-
`
|
|
3339
|
+
{{application}}`
|
|
3076
3340
|
}),
|
|
3077
3341
|
appBrowserConstants: Object.freeze({
|
|
3078
3342
|
id: "appBrowserConstants",
|
|
@@ -3097,6 +3361,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH = 2
|
|
|
3097
3361
|
category: "source",
|
|
3098
3362
|
placeholders: Object.freeze([]),
|
|
3099
3363
|
content: `import type { BrowserApplicationErrorCode, BrowserApplicationErrorContext } from './types.js'
|
|
3364
|
+
import { holds } from '@orkestrel/contract'
|
|
3100
3365
|
|
|
3101
3366
|
/** A rejected browser application configuration value. */
|
|
3102
3367
|
${EXPORT_KEYWORD} class BrowserApplicationError extends Error {
|
|
@@ -3129,11 +3394,7 @@ ${EXPORT_KEYWORD} class BrowserApplicationError extends Error {
|
|
|
3129
3394
|
* \`\`\`
|
|
3130
3395
|
*/
|
|
3131
3396
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserApplicationError(value: unknown): value is BrowserApplicationError {
|
|
3132
|
-
|
|
3133
|
-
return value instanceof BrowserApplicationError
|
|
3134
|
-
} catch {
|
|
3135
|
-
return false
|
|
3136
|
-
}
|
|
3397
|
+
return holds(() => value instanceof BrowserApplicationError)
|
|
3137
3398
|
}
|
|
3138
3399
|
`
|
|
3139
3400
|
}),
|
|
@@ -3144,6 +3405,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserApplicationError(value: unknown):
|
|
|
3144
3405
|
category: "source",
|
|
3145
3406
|
placeholders: Object.freeze([]),
|
|
3146
3407
|
content: `import type { BrowserApplicationOptions } from './types.js'
|
|
3408
|
+
import { isNonEmptyString } from '@orkestrel/contract'
|
|
3147
3409
|
import {
|
|
3148
3410
|
MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH,
|
|
3149
3411
|
MAX_BROWSER_APPLICATION_NAME_LENGTH,
|
|
@@ -3180,6 +3442,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
|
|
|
3180
3442
|
{ value },
|
|
3181
3443
|
)
|
|
3182
3444
|
}
|
|
3445
|
+
// Walk own descriptors directly: reading values through a getter would run caller code.
|
|
3183
3446
|
const keys = Reflect.ownKeys(value)
|
|
3184
3447
|
if (keys.some((key) => key !== 'name')) {
|
|
3185
3448
|
throw new BrowserApplicationError('CONFIG', 'Unknown browser application option', {
|
|
@@ -3204,7 +3467,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
|
|
|
3204
3467
|
})
|
|
3205
3468
|
}
|
|
3206
3469
|
const name = descriptor.value.trim()
|
|
3207
|
-
if (name
|
|
3470
|
+
if (!isNonEmptyString(name) || name.length > MAX_BROWSER_APPLICATION_NAME_LENGTH) {
|
|
3208
3471
|
throw new BrowserApplicationError(
|
|
3209
3472
|
'CONFIG',
|
|
3210
3473
|
\`Browser application name must contain 1 through \${MAX_BROWSER_APPLICATION_NAME_LENGTH} characters\`,
|
|
@@ -3221,22 +3484,73 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
|
|
|
3221
3484
|
}
|
|
3222
3485
|
`
|
|
3223
3486
|
}),
|
|
3224
|
-
|
|
3225
|
-
id: "
|
|
3226
|
-
name: "
|
|
3227
|
-
summary: "The
|
|
3487
|
+
appBrowserSeeders: Object.freeze({
|
|
3488
|
+
id: "appBrowserSeeders",
|
|
3489
|
+
name: "appBrowserSeeders",
|
|
3490
|
+
summary: "The frozen, inert identity the showcase renders.",
|
|
3228
3491
|
category: "source",
|
|
3229
3492
|
placeholders: Object.freeze([Object.freeze({
|
|
3493
|
+
name: "applicationImport",
|
|
3494
|
+
description: "The selected layer type import for the root-view identity."
|
|
3495
|
+
}), Object.freeze({
|
|
3230
3496
|
name: "nameImport",
|
|
3231
3497
|
description: "The selected layer import for APP_NAME."
|
|
3232
3498
|
})]),
|
|
3499
|
+
content: `{{applicationImport}}
|
|
3500
|
+
{{nameImport}}
|
|
3501
|
+
|
|
3502
|
+
/**
|
|
3503
|
+
* Seed the inert identity the showcase renders.
|
|
3504
|
+
*
|
|
3505
|
+
* @returns A fresh frozen identity, identical on every call.
|
|
3506
|
+
*
|
|
3507
|
+
* @remarks
|
|
3508
|
+
* The showcase's only data. It is exactly the value the shipped root view receives from
|
|
3509
|
+
* the running application, so the showcase exercises the shipped view rather than a
|
|
3510
|
+
* parallel copy of it.
|
|
3511
|
+
*
|
|
3512
|
+
* @example
|
|
3513
|
+
* \`\`\`ts
|
|
3514
|
+
* import { seedApplication } from '@app/browser'
|
|
3515
|
+
*
|
|
3516
|
+
* seedApplication().name // the seeded showcase identity
|
|
3517
|
+
* \`\`\`
|
|
3518
|
+
*/
|
|
3519
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} seedApplication(): Application {
|
|
3520
|
+
return Object.freeze({ name: \`\${APP_NAME} showcase\` })
|
|
3521
|
+
}
|
|
3522
|
+
`
|
|
3523
|
+
}),
|
|
3524
|
+
appBrowserFactories: Object.freeze({
|
|
3525
|
+
id: "appBrowserFactories",
|
|
3526
|
+
name: "appBrowserFactories",
|
|
3527
|
+
summary: "The Vue browser application factory.",
|
|
3528
|
+
category: "source",
|
|
3529
|
+
placeholders: Object.freeze([
|
|
3530
|
+
Object.freeze({
|
|
3531
|
+
name: "nameImport",
|
|
3532
|
+
description: "The selected layer import for APP_NAME."
|
|
3533
|
+
}),
|
|
3534
|
+
Object.freeze({
|
|
3535
|
+
name: "seedImport",
|
|
3536
|
+
description: "The optional showcase seeder import."
|
|
3537
|
+
}),
|
|
3538
|
+
Object.freeze({
|
|
3539
|
+
name: "showcase",
|
|
3540
|
+
description: "The optional seeded showcase factory."
|
|
3541
|
+
}),
|
|
3542
|
+
Object.freeze({
|
|
3543
|
+
name: "boundary",
|
|
3544
|
+
description: "The optional server-boundary startup factory."
|
|
3545
|
+
})
|
|
3546
|
+
]),
|
|
3233
3547
|
content: `import type { App } from 'vue'
|
|
3234
3548
|
import type { BrowserApplicationOptions } from './types.js'
|
|
3235
3549
|
import { createApp } from 'vue'
|
|
3236
3550
|
import ApplicationView from './ApplicationView.vue'
|
|
3237
3551
|
{{nameImport}}
|
|
3238
3552
|
import { parseBrowserApplicationOptions } from './parsers.js'
|
|
3239
|
-
|
|
3553
|
+
{{seedImport}}
|
|
3240
3554
|
/**
|
|
3241
3555
|
* Create an unmounted Vue application.
|
|
3242
3556
|
*
|
|
@@ -3254,19 +3568,22 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createBrowserApplication(options: BrowserA
|
|
|
3254
3568
|
const parsed = parseBrowserApplicationOptions(options)
|
|
3255
3569
|
return createApp(ApplicationView, { name: parsed.name ?? APP_NAME })
|
|
3256
3570
|
}
|
|
3257
|
-
`
|
|
3571
|
+
{{showcase}}{{boundary}}`
|
|
3258
3572
|
}),
|
|
3259
3573
|
appBrowserIndex: Object.freeze({
|
|
3260
3574
|
id: "appBrowserIndex",
|
|
3261
3575
|
name: "appBrowserIndex",
|
|
3262
3576
|
summary: "The browser application barrel.",
|
|
3263
3577
|
category: "source",
|
|
3264
|
-
placeholders: Object.freeze([
|
|
3578
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3579
|
+
name: "seeders",
|
|
3580
|
+
description: "The optional showcase seeder barrel row."
|
|
3581
|
+
})]),
|
|
3265
3582
|
content: `export * from './types.js'
|
|
3266
3583
|
export * from './constants.js'
|
|
3267
3584
|
export * from './errors.js'
|
|
3268
3585
|
export * from './parsers.js'
|
|
3269
|
-
export * from './factories.js'
|
|
3586
|
+
{{seeders}}export * from './factories.js'
|
|
3270
3587
|
`
|
|
3271
3588
|
}),
|
|
3272
3589
|
appBrowserMain: Object.freeze({
|
|
@@ -3274,10 +3591,27 @@ export * from './factories.js'
|
|
|
3274
3591
|
name: "appBrowserMain",
|
|
3275
3592
|
summary: "The browser executable entry.",
|
|
3276
3593
|
category: "source",
|
|
3594
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3595
|
+
name: "factory",
|
|
3596
|
+
description: "The factory the shipped entry mounts through."
|
|
3597
|
+
}), Object.freeze({
|
|
3598
|
+
name: "mount",
|
|
3599
|
+
description: "The mounting statement."
|
|
3600
|
+
})]),
|
|
3601
|
+
content: `import { {{factory}} } from './index.js'
|
|
3602
|
+
|
|
3603
|
+
{{mount}}
|
|
3604
|
+
`
|
|
3605
|
+
}),
|
|
3606
|
+
appBrowserShowcase: Object.freeze({
|
|
3607
|
+
id: "appBrowserShowcase",
|
|
3608
|
+
name: "appBrowserShowcase",
|
|
3609
|
+
summary: "The showcase executable entry.",
|
|
3610
|
+
category: "source",
|
|
3277
3611
|
placeholders: Object.freeze([]),
|
|
3278
|
-
content: `import {
|
|
3612
|
+
content: `import { mountShowcaseApplication } from './index.js'
|
|
3279
3613
|
|
|
3280
|
-
|
|
3614
|
+
mountShowcaseApplication('#app')
|
|
3281
3615
|
`
|
|
3282
3616
|
}),
|
|
3283
3617
|
appBrowserView: Object.freeze({
|
|
@@ -3322,6 +3656,33 @@ defineProps<{ readonly name: string }>()
|
|
|
3322
3656
|
<script type="module" src="/main.ts"><\/script>
|
|
3323
3657
|
</body>
|
|
3324
3658
|
</html>
|
|
3659
|
+
`
|
|
3660
|
+
}),
|
|
3661
|
+
appBrowserShowcaseHtml: Object.freeze({
|
|
3662
|
+
id: "appBrowserShowcaseHtml",
|
|
3663
|
+
name: "appBrowserShowcaseHtml",
|
|
3664
|
+
summary: "The showcase HTML entry with its development security policy.",
|
|
3665
|
+
category: "source",
|
|
3666
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3667
|
+
name: "name",
|
|
3668
|
+
description: "The application name."
|
|
3669
|
+
})]),
|
|
3670
|
+
content: `<!doctype html>
|
|
3671
|
+
<html lang="en">
|
|
3672
|
+
<head>
|
|
3673
|
+
<meta
|
|
3674
|
+
http-equiv="Content-Security-Policy"
|
|
3675
|
+
content="default-src 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src data:; font-src data:; script-src-attr 'none'"
|
|
3676
|
+
/>
|
|
3677
|
+
<meta charset="UTF-8" />
|
|
3678
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
3679
|
+
<title>{{name}} showcase</title>
|
|
3680
|
+
</head>
|
|
3681
|
+
<body>
|
|
3682
|
+
<div id="app"></div>
|
|
3683
|
+
<script type="module" src="/showcase.ts"><\/script>
|
|
3684
|
+
</body>
|
|
3685
|
+
</html>
|
|
3325
3686
|
`
|
|
3326
3687
|
}),
|
|
3327
3688
|
appBrowserEnv: Object.freeze({
|
|
@@ -3345,8 +3706,20 @@ declare module '*.vue' {
|
|
|
3345
3706
|
name: "appServerTypes",
|
|
3346
3707
|
summary: "The application server contract.",
|
|
3347
3708
|
category: "source",
|
|
3348
|
-
placeholders: Object.freeze([
|
|
3349
|
-
|
|
3709
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3710
|
+
name: "record",
|
|
3711
|
+
description: "The health record, declared here only while the server alone reads it."
|
|
3712
|
+
})]),
|
|
3713
|
+
content: `import type { EmitterErrorHandler, EmitterHooks, EmitterInterface } from '@orkestrel/emitter'
|
|
3714
|
+
import type { IdentifierState } from '@orkestrel/middleware'
|
|
3715
|
+
import type { ConnectionInfo, ServerStatus } from '@orkestrel/server'
|
|
3716
|
+
|
|
3717
|
+
{{record}}/** Per-request application state derived from connection facts. */
|
|
3718
|
+
${EXPORT_KEYWORD} interface ApplicationState extends IdentifierState {
|
|
3719
|
+
readonly connection: ConnectionInfo
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
/** A rejected application server boundary. */
|
|
3350
3723
|
${EXPORT_KEYWORD} type ApplicationServerErrorCode = 'CONFIG' | 'LIFECYCLE'
|
|
3351
3724
|
|
|
3352
3725
|
/** Diagnostic context attached to an application server boundary error. */
|
|
@@ -3357,26 +3730,49 @@ ${EXPORT_KEYWORD} interface ApplicationServerErrorContext {
|
|
|
3357
3730
|
|
|
3358
3731
|
/** Options for creating an application server. */
|
|
3359
3732
|
${EXPORT_KEYWORD} interface ApplicationServerOptions {
|
|
3360
|
-
readonly
|
|
3361
|
-
|
|
3362
|
-
|
|
3733
|
+
readonly server?: {
|
|
3734
|
+
readonly host?: string
|
|
3735
|
+
readonly port?: number
|
|
3736
|
+
readonly timeout?: number
|
|
3737
|
+
}
|
|
3363
3738
|
}
|
|
3364
3739
|
|
|
3365
3740
|
/** A lifecycle-safe application server. */
|
|
3366
3741
|
${EXPORT_KEYWORD} interface ApplicationServerInterface {
|
|
3367
3742
|
readonly host: string
|
|
3368
|
-
readonly port: number
|
|
3369
|
-
readonly
|
|
3370
|
-
readonly url: string
|
|
3743
|
+
readonly port: number | undefined
|
|
3744
|
+
readonly status: ServerStatus
|
|
3745
|
+
readonly url: string | undefined
|
|
3371
3746
|
start(signal?: AbortSignal): Promise<void>
|
|
3372
3747
|
stop(): Promise<void>
|
|
3748
|
+
destroy(): Promise<void>
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
/** Observable process-lifecycle outcomes for an application server runner. */
|
|
3752
|
+
${EXPORT_KEYWORD} type ApplicationServerRunnerEventMap = {
|
|
3753
|
+
readonly ready: readonly [url: string]
|
|
3754
|
+
readonly fail: readonly [error: unknown]
|
|
3373
3755
|
}
|
|
3374
3756
|
|
|
3375
3757
|
/** The process lifecycle owner for an application server. */
|
|
3376
3758
|
${EXPORT_KEYWORD} interface ApplicationServerRunnerInterface {
|
|
3759
|
+
readonly emitter: EmitterInterface<ApplicationServerRunnerEventMap>
|
|
3377
3760
|
start(): void
|
|
3378
3761
|
stop(): Promise<void>
|
|
3379
3762
|
}
|
|
3763
|
+
|
|
3764
|
+
/**
|
|
3765
|
+
* Options for observing an application server runner.
|
|
3766
|
+
*
|
|
3767
|
+
* @remarks
|
|
3768
|
+
* Initial \`on\` hooks run before the runner's own readiness and failure effects. When no earlier
|
|
3769
|
+
* failure set an exit code, a synchronous fail hook therefore observes \`process.exitCode\` as
|
|
3770
|
+
* \`undefined\` before the default reporter sets it to \`1\`.
|
|
3771
|
+
*/
|
|
3772
|
+
${EXPORT_KEYWORD} interface ApplicationServerRunnerOptions {
|
|
3773
|
+
readonly on?: EmitterHooks<ApplicationServerRunnerEventMap>
|
|
3774
|
+
readonly error?: EmitterErrorHandler
|
|
3775
|
+
}
|
|
3380
3776
|
`
|
|
3381
3777
|
}),
|
|
3382
3778
|
appServerConstants: Object.freeze({
|
|
@@ -3387,6 +3783,9 @@ ${EXPORT_KEYWORD} interface ApplicationServerRunnerInterface {
|
|
|
3387
3783
|
placeholders: Object.freeze([Object.freeze({
|
|
3388
3784
|
name: "nameConstant",
|
|
3389
3785
|
description: "The optional server-only APP_NAME declaration."
|
|
3786
|
+
}), Object.freeze({
|
|
3787
|
+
name: "health",
|
|
3788
|
+
description: "The health route constants, declared here only while the server alone reads them."
|
|
3390
3789
|
})]),
|
|
3391
3790
|
content: `{{nameConstant}}/** The fail-closed loopback host default. */
|
|
3392
3791
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} DEFAULT_APP_HOST = '127.0.0.1'
|
|
@@ -3406,24 +3805,6 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APP_HOST_INPUT_LENGTH = 255
|
|
|
3406
3805
|
/** Maximum raw characters inspected at an application numeric boundary. */
|
|
3407
3806
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APP_NUMBER_INPUT_LENGTH = 32
|
|
3408
3807
|
|
|
3409
|
-
/** Maximum simultaneous connections accepted by the generated server. */
|
|
3410
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_CONNECTIONS = 16
|
|
3411
|
-
|
|
3412
|
-
/** Maximum request headers accepted before Node rejects the request. */
|
|
3413
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_HEADERS = 100
|
|
3414
|
-
|
|
3415
|
-
/** Maximum milliseconds allowed to receive complete request headers. */
|
|
3416
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEADERS_TIMEOUT = 10_000
|
|
3417
|
-
|
|
3418
|
-
/** Maximum milliseconds allowed for one complete request. */
|
|
3419
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_REQUEST_TIMEOUT = 30_000
|
|
3420
|
-
|
|
3421
|
-
/** Idle keep-alive milliseconds before a connection is closed. */
|
|
3422
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_KEEP_ALIVE_TIMEOUT = 5_000
|
|
3423
|
-
|
|
3424
|
-
/** Maximum requests served through one keep-alive connection. */
|
|
3425
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_MAX_REQUESTS_PER_SOCKET = 100
|
|
3426
|
-
|
|
3427
3808
|
/** The decimal-only syntax accepted at the APP_PORT string boundary. */
|
|
3428
3809
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_PORT_PATTERN = /^\\d+$/
|
|
3429
3810
|
|
|
@@ -3432,13 +3813,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HOST_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Z
|
|
|
3432
3813
|
|
|
3433
3814
|
/** Numeric-looking non-IP hosts rejected before platform DNS interpretation. */
|
|
3434
3815
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NUMERIC_HOST_PATTERN = /^[0-9.]+$/
|
|
3435
|
-
|
|
3436
|
-
/** The only HTTP method owned by the application health route. */
|
|
3437
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_METHOD = 'GET'
|
|
3438
|
-
|
|
3439
|
-
/** The only HTTP path owned by the generated application server. */
|
|
3440
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/'
|
|
3441
|
-
`
|
|
3816
|
+
{{health}}`
|
|
3442
3817
|
}),
|
|
3443
3818
|
appServerErrors: Object.freeze({
|
|
3444
3819
|
id: "appServerErrors",
|
|
@@ -3447,6 +3822,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/'
|
|
|
3447
3822
|
category: "source",
|
|
3448
3823
|
placeholders: Object.freeze([]),
|
|
3449
3824
|
content: `import type { ApplicationServerErrorCode, ApplicationServerErrorContext } from './types.js'
|
|
3825
|
+
import { holds } from '@orkestrel/contract'
|
|
3450
3826
|
|
|
3451
3827
|
/** A rejected application server configuration or lifecycle operation. */
|
|
3452
3828
|
${EXPORT_KEYWORD} class ApplicationServerError extends Error {
|
|
@@ -3479,11 +3855,7 @@ ${EXPORT_KEYWORD} class ApplicationServerError extends Error {
|
|
|
3479
3855
|
* \`\`\`
|
|
3480
3856
|
*/
|
|
3481
3857
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationServerError(value: unknown): value is ApplicationServerError {
|
|
3482
|
-
|
|
3483
|
-
return value instanceof ApplicationServerError
|
|
3484
|
-
} catch {
|
|
3485
|
-
return false
|
|
3486
|
-
}
|
|
3858
|
+
return holds(() => value instanceof ApplicationServerError)
|
|
3487
3859
|
}
|
|
3488
3860
|
`
|
|
3489
3861
|
}),
|
|
@@ -3494,6 +3866,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationServerError(value: unknown):
|
|
|
3494
3866
|
category: "source",
|
|
3495
3867
|
placeholders: Object.freeze([]),
|
|
3496
3868
|
content: `import type { ApplicationServerOptions } from './types.js'
|
|
3869
|
+
import { isNonEmptyString, parseString } from '@orkestrel/contract'
|
|
3497
3870
|
import { isIP } from 'node:net'
|
|
3498
3871
|
import {
|
|
3499
3872
|
APP_HOST_LABEL_PATTERN,
|
|
@@ -3523,7 +3896,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationHost(value: unknown): stri
|
|
|
3523
3896
|
const host = value.trim()
|
|
3524
3897
|
const family = isIP(host)
|
|
3525
3898
|
if (
|
|
3526
|
-
host
|
|
3899
|
+
!isNonEmptyString(host) ||
|
|
3527
3900
|
host.length > 253 ||
|
|
3528
3901
|
(family === 0 &&
|
|
3529
3902
|
(APP_NUMERIC_HOST_PATTERN.test(host) ||
|
|
@@ -3545,18 +3918,14 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationHost(value: unknown): stri
|
|
|
3545
3918
|
*/
|
|
3546
3919
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationPort(value: unknown): number {
|
|
3547
3920
|
if (value === undefined) return DEFAULT_APP_PORT
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
value,
|
|
3551
|
-
})
|
|
3552
|
-
}
|
|
3553
|
-
if (typeof value === 'string' && value.length > MAX_APP_NUMBER_INPUT_LENGTH) {
|
|
3921
|
+
const text = parseString(value)
|
|
3922
|
+
if (text === undefined || text.length > MAX_APP_NUMBER_INPUT_LENGTH) {
|
|
3554
3923
|
throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
|
|
3555
3924
|
value,
|
|
3556
3925
|
})
|
|
3557
3926
|
}
|
|
3558
|
-
const
|
|
3559
|
-
const port = APP_PORT_PATTERN.test(
|
|
3927
|
+
const normalized = text.trim()
|
|
3928
|
+
const port = APP_PORT_PATTERN.test(normalized) ? Number(normalized) : Number.NaN
|
|
3560
3929
|
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
3561
3930
|
throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
|
|
3562
3931
|
value,
|
|
@@ -3574,22 +3943,16 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationPort(value: unknown): numb
|
|
|
3574
3943
|
*/
|
|
3575
3944
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationStartTimeout(value: unknown): number {
|
|
3576
3945
|
if (value === undefined) return DEFAULT_APP_START_TIMEOUT
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
'CONFIG',
|
|
3580
|
-
\`APP_START_TIMEOUT must be an integer from 1 through \${MAX_APP_START_TIMEOUT}\`,
|
|
3581
|
-
{ value },
|
|
3582
|
-
)
|
|
3583
|
-
}
|
|
3584
|
-
if (typeof value === 'string' && value.length > MAX_APP_NUMBER_INPUT_LENGTH) {
|
|
3946
|
+
const text = parseString(value)
|
|
3947
|
+
if (text === undefined || text.length > MAX_APP_NUMBER_INPUT_LENGTH) {
|
|
3585
3948
|
throw new ApplicationServerError(
|
|
3586
3949
|
'CONFIG',
|
|
3587
3950
|
\`APP_START_TIMEOUT must be an integer from 1 through \${MAX_APP_START_TIMEOUT}\`,
|
|
3588
3951
|
{ value },
|
|
3589
3952
|
)
|
|
3590
3953
|
}
|
|
3591
|
-
const
|
|
3592
|
-
const timeout = APP_PORT_PATTERN.test(
|
|
3954
|
+
const normalized = text.trim()
|
|
3955
|
+
const timeout = APP_PORT_PATTERN.test(normalized) ? Number(normalized) : Number.NaN
|
|
3593
3956
|
if (!Number.isInteger(timeout) || timeout < 1 || timeout > MAX_APP_START_TIMEOUT) {
|
|
3594
3957
|
throw new ApplicationServerError(
|
|
3595
3958
|
'CONFIG',
|
|
@@ -3625,24 +3988,58 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
|
|
|
3625
3988
|
)
|
|
3626
3989
|
}
|
|
3627
3990
|
const keys = Reflect.ownKeys(value)
|
|
3628
|
-
const unknown = keys.filter((key) => key !== '
|
|
3991
|
+
const unknown = keys.filter((key) => key !== 'server')
|
|
3629
3992
|
if (unknown.length > 0) {
|
|
3630
3993
|
throw new ApplicationServerError('CONFIG', 'Unknown application server option', { value })
|
|
3631
3994
|
}
|
|
3632
|
-
const
|
|
3633
|
-
|
|
3634
|
-
|
|
3995
|
+
const serverDescriptor = Reflect.getOwnPropertyDescriptor(value, 'server')
|
|
3996
|
+
if (
|
|
3997
|
+
keys.includes('server') &&
|
|
3998
|
+
(serverDescriptor === undefined || !Reflect.has(serverDescriptor, 'value'))
|
|
3999
|
+
) {
|
|
4000
|
+
throw new ApplicationServerError(
|
|
4001
|
+
'CONFIG',
|
|
4002
|
+
'Application server options must use data properties',
|
|
4003
|
+
{ value },
|
|
4004
|
+
)
|
|
4005
|
+
}
|
|
4006
|
+
const server = serverDescriptor?.value
|
|
4007
|
+
if (server === undefined) return {}
|
|
4008
|
+
if (typeof server !== 'object' || server === null || Array.isArray(server)) {
|
|
4009
|
+
throw new ApplicationServerError('CONFIG', 'Application server settings must be an object', {
|
|
4010
|
+
value,
|
|
4011
|
+
})
|
|
4012
|
+
}
|
|
4013
|
+
const serverPrototype = Reflect.getPrototypeOf(server)
|
|
4014
|
+
if (serverPrototype !== Object.prototype && serverPrototype !== null) {
|
|
4015
|
+
throw new ApplicationServerError(
|
|
4016
|
+
'CONFIG',
|
|
4017
|
+
'Application server settings must be a plain record',
|
|
4018
|
+
{ value },
|
|
4019
|
+
)
|
|
4020
|
+
}
|
|
4021
|
+
// Walk own descriptors directly: reading values through a getter would run caller code.
|
|
4022
|
+
const serverKeys = Reflect.ownKeys(server)
|
|
4023
|
+
const serverUnknown = serverKeys.filter(
|
|
4024
|
+
(key) => key !== 'host' && key !== 'port' && key !== 'timeout',
|
|
4025
|
+
)
|
|
4026
|
+
if (serverUnknown.length > 0) {
|
|
4027
|
+
throw new ApplicationServerError('CONFIG', 'Unknown application server setting', { value })
|
|
4028
|
+
}
|
|
4029
|
+
const hostDescriptor = Reflect.getOwnPropertyDescriptor(server, 'host')
|
|
4030
|
+
const portDescriptor = Reflect.getOwnPropertyDescriptor(server, 'port')
|
|
4031
|
+
const timeoutDescriptor = Reflect.getOwnPropertyDescriptor(server, 'timeout')
|
|
3635
4032
|
if (
|
|
3636
|
-
(
|
|
4033
|
+
(serverKeys.includes('host') &&
|
|
3637
4034
|
(hostDescriptor === undefined || !Reflect.has(hostDescriptor, 'value'))) ||
|
|
3638
|
-
(
|
|
4035
|
+
(serverKeys.includes('port') &&
|
|
3639
4036
|
(portDescriptor === undefined || !Reflect.has(portDescriptor, 'value'))) ||
|
|
3640
|
-
(
|
|
4037
|
+
(serverKeys.includes('timeout') &&
|
|
3641
4038
|
(timeoutDescriptor === undefined || !Reflect.has(timeoutDescriptor, 'value')))
|
|
3642
4039
|
) {
|
|
3643
4040
|
throw new ApplicationServerError(
|
|
3644
4041
|
'CONFIG',
|
|
3645
|
-
'Application server
|
|
4042
|
+
'Application server settings must use data properties',
|
|
3646
4043
|
{ value },
|
|
3647
4044
|
)
|
|
3648
4045
|
}
|
|
@@ -3650,9 +4047,11 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
|
|
|
3650
4047
|
const port = portDescriptor?.value
|
|
3651
4048
|
const timeout = timeoutDescriptor?.value
|
|
3652
4049
|
return {
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4050
|
+
server: {
|
|
4051
|
+
...(host === undefined ? {} : { host: parseApplicationHost(host) }),
|
|
4052
|
+
...(port === undefined ? {} : { port: parseApplicationPort(port) }),
|
|
4053
|
+
...(timeout === undefined ? {} : { timeout: parseApplicationStartTimeout(timeout) }),
|
|
4054
|
+
},
|
|
3656
4055
|
}
|
|
3657
4056
|
} catch (error) {
|
|
3658
4057
|
if (isApplicationServerError(error)) throw error
|
|
@@ -3661,47 +4060,56 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
|
|
|
3661
4060
|
})
|
|
3662
4061
|
}
|
|
3663
4062
|
}
|
|
4063
|
+
`
|
|
4064
|
+
}),
|
|
4065
|
+
appServerRoutes: Object.freeze({
|
|
4066
|
+
id: "appServerRoutes",
|
|
4067
|
+
name: "appServerRoutes",
|
|
4068
|
+
summary: "The standalone application route dispatcher factory.",
|
|
4069
|
+
category: "source",
|
|
4070
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4071
|
+
name: "healthImport",
|
|
4072
|
+
description: "The selected layer import for the health route constants."
|
|
4073
|
+
})]),
|
|
4074
|
+
content: `import type { DispatcherInterface } from '@orkestrel/router'
|
|
4075
|
+
import type { ApplicationState } from './types.js'
|
|
4076
|
+
import { createDispatcher } from '@orkestrel/router'
|
|
4077
|
+
{{healthImport}}
|
|
4078
|
+
import { handleApplicationHealth } from './handlers.js'
|
|
4079
|
+
|
|
4080
|
+
/** Create a fresh dispatcher for the generated application's fetch-standard health boundary. */
|
|
4081
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationDispatcher(): DispatcherInterface<ApplicationState> {
|
|
4082
|
+
return createDispatcher<ApplicationState>({
|
|
4083
|
+
routes: [
|
|
4084
|
+
{
|
|
4085
|
+
method: APP_HEALTH_METHOD,
|
|
4086
|
+
path: APP_HEALTH_PATH,
|
|
4087
|
+
handler: handleApplicationHealth,
|
|
4088
|
+
},
|
|
4089
|
+
],
|
|
4090
|
+
})
|
|
4091
|
+
}
|
|
3664
4092
|
`
|
|
3665
4093
|
}),
|
|
3666
4094
|
appServerHandlers: Object.freeze({
|
|
3667
4095
|
id: "appServerHandlers",
|
|
3668
4096
|
name: "appServerHandlers",
|
|
3669
|
-
summary: "The server
|
|
4097
|
+
summary: "The application server diagnostic reporter.",
|
|
3670
4098
|
category: "source",
|
|
3671
4099
|
placeholders: Object.freeze([Object.freeze({
|
|
4100
|
+
name: "recordImport",
|
|
4101
|
+
description: "The selected layer type import for the health record."
|
|
4102
|
+
}), Object.freeze({
|
|
3672
4103
|
name: "nameImport",
|
|
3673
4104
|
description: "The selected layer import for APP_NAME."
|
|
3674
4105
|
})]),
|
|
3675
|
-
content: `
|
|
4106
|
+
content: `{{recordImport}}
|
|
3676
4107
|
{{nameImport}}
|
|
3677
|
-
import { APP_HEALTH_METHOD, APP_HEALTH_PATH } from './constants.js'
|
|
3678
4108
|
import { isApplicationServerError } from './errors.js'
|
|
3679
4109
|
|
|
3680
|
-
/**
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
* @param request - The incoming Node request.
|
|
3684
|
-
* @param response - The Node response to complete exactly once.
|
|
3685
|
-
*/
|
|
3686
|
-
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} handleApplicationRequest(request: IncomingMessage, response: ServerResponse): void {
|
|
3687
|
-
if (request.method !== APP_HEALTH_METHOD) {
|
|
3688
|
-
response.writeHead(405, {
|
|
3689
|
-
allow: APP_HEALTH_METHOD,
|
|
3690
|
-
'content-type': 'text/plain; charset=utf-8',
|
|
3691
|
-
})
|
|
3692
|
-
response.end('Method Not Allowed')
|
|
3693
|
-
return
|
|
3694
|
-
}
|
|
3695
|
-
if (request.url !== APP_HEALTH_PATH) {
|
|
3696
|
-
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
|
|
3697
|
-
response.end('Not Found')
|
|
3698
|
-
return
|
|
3699
|
-
}
|
|
3700
|
-
response.writeHead(200, {
|
|
3701
|
-
'cache-control': 'no-store',
|
|
3702
|
-
'content-type': 'application/json; charset=utf-8',
|
|
3703
|
-
})
|
|
3704
|
-
response.end(JSON.stringify({ name: APP_NAME, status: 'ok' }))
|
|
4110
|
+
/** Return the generated application's shared health record. */
|
|
4111
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} handleApplicationHealth(): Response {
|
|
4112
|
+
return Response.json({ name: APP_NAME, status: 'ok' } satisfies ApplicationRecord)
|
|
3705
4113
|
}
|
|
3706
4114
|
|
|
3707
4115
|
/**
|
|
@@ -3729,8 +4137,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} reportApplicationServerError(error: unknow
|
|
|
3729
4137
|
} catch {
|
|
3730
4138
|
message = '[ERROR] Application server failed'
|
|
3731
4139
|
}
|
|
3732
|
-
process.stderr.write(\`\${message}\\n\`)
|
|
3733
4140
|
process.exitCode = 1
|
|
4141
|
+
process.stderr.write(\`\${message}\\n\`)
|
|
3734
4142
|
}
|
|
3735
4143
|
`
|
|
3736
4144
|
}),
|
|
@@ -3740,176 +4148,106 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} reportApplicationServerError(error: unknow
|
|
|
3740
4148
|
summary: "The application server implementation.",
|
|
3741
4149
|
category: "source",
|
|
3742
4150
|
placeholders: Object.freeze([]),
|
|
3743
|
-
content: `import type {
|
|
3744
|
-
import type {
|
|
3745
|
-
import {
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
APP_MAX_REQUESTS_PER_SOCKET,
|
|
3754
|
-
APP_REQUEST_TIMEOUT,
|
|
3755
|
-
} from './constants.js'
|
|
3756
|
-
import { ApplicationServerError, isApplicationServerError } from './errors.js'
|
|
3757
|
-
import { handleApplicationRequest } from './handlers.js'
|
|
4151
|
+
content: `import type { DispatcherInterface } from '@orkestrel/router'
|
|
4152
|
+
import type { ConnectionInfo, ServerInterface } from '@orkestrel/server'
|
|
4153
|
+
import type {
|
|
4154
|
+
ApplicationServerInterface,
|
|
4155
|
+
ApplicationServerOptions,
|
|
4156
|
+
ApplicationState,
|
|
4157
|
+
} from './types.js'
|
|
4158
|
+
import { createBoundary, createDeadline, createSecurity } from '@orkestrel/middleware'
|
|
4159
|
+
import { createServer } from '@orkestrel/server'
|
|
4160
|
+
import { ApplicationServerError } from './errors.js'
|
|
3758
4161
|
import {
|
|
3759
4162
|
parseApplicationHost,
|
|
3760
4163
|
parseApplicationPort,
|
|
3761
4164
|
parseApplicationServerOptions,
|
|
3762
4165
|
parseApplicationStartTimeout,
|
|
3763
4166
|
} from './parsers.js'
|
|
4167
|
+
import { createApplicationDispatcher } from './routes.js'
|
|
3764
4168
|
|
|
3765
|
-
/** A repeat-safe
|
|
4169
|
+
/** A repeat-safe application server composed from the installed server substrate. */
|
|
3766
4170
|
${EXPORT_KEYWORD} class ApplicationServer implements ApplicationServerInterface {
|
|
3767
4171
|
readonly host: string
|
|
3768
|
-
readonly #
|
|
3769
|
-
readonly #
|
|
3770
|
-
#port: number
|
|
3771
|
-
readonly #server: Server
|
|
3772
|
-
#transition: Promise<void> = Promise.resolve()
|
|
3773
|
-
readonly #starts = new Set<AbortController>()
|
|
3774
|
-
readonly #stopped = new WeakSet<AbortController>()
|
|
4172
|
+
readonly #dispatcher: DispatcherInterface<ApplicationState>
|
|
4173
|
+
readonly #server: ServerInterface<ApplicationState>
|
|
3775
4174
|
|
|
3776
4175
|
constructor(options: ApplicationServerOptions = {}) {
|
|
3777
4176
|
const parsed = parseApplicationServerOptions(options)
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
4177
|
+
const server = parsed.server
|
|
4178
|
+
const host = parseApplicationHost(
|
|
4179
|
+
server?.host === undefined ? process.env.APP_HOST : server.host,
|
|
3781
4180
|
)
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
parsed.timeout === undefined ? process.env.APP_START_TIMEOUT : parsed.timeout,
|
|
4181
|
+
const port = parseApplicationPort(
|
|
4182
|
+
server?.port === undefined ? process.env.APP_PORT : server.port,
|
|
3785
4183
|
)
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
this
|
|
3790
|
-
this.#
|
|
3791
|
-
this.#server
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
4184
|
+
const timeout = parseApplicationStartTimeout(
|
|
4185
|
+
server?.timeout === undefined ? process.env.APP_START_TIMEOUT : server.timeout,
|
|
4186
|
+
)
|
|
4187
|
+
this.host = host
|
|
4188
|
+
this.#dispatcher = createApplicationDispatcher()
|
|
4189
|
+
this.#server = createServer<ApplicationState>({
|
|
4190
|
+
dispatcher: this.#dispatcher,
|
|
4191
|
+
state: ApplicationServer.#state,
|
|
4192
|
+
middleware: [
|
|
4193
|
+
createBoundary<ApplicationState>(),
|
|
4194
|
+
createSecurity<ApplicationState>(),
|
|
4195
|
+
createDeadline<ApplicationState>({ ms: timeout }),
|
|
4196
|
+
],
|
|
4197
|
+
host,
|
|
4198
|
+
port,
|
|
4199
|
+
timeouts: { start: timeout },
|
|
4200
|
+
})
|
|
3801
4201
|
}
|
|
3802
4202
|
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
return \`http://\${hostname}:\${this.port}\`
|
|
4203
|
+
static #state(connection: ConnectionInfo): ApplicationState {
|
|
4204
|
+
return { connection }
|
|
3806
4205
|
}
|
|
3807
4206
|
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
this.#starts.add(controller)
|
|
3811
|
-
const queued = this.#queue(this.#start.bind(this, controller, signal))
|
|
3812
|
-
void queued.then(this.#settle.bind(this, controller), this.#settle.bind(this, controller))
|
|
3813
|
-
return queued
|
|
4207
|
+
get port(): number | undefined {
|
|
4208
|
+
return this.#server.port
|
|
3814
4209
|
}
|
|
3815
4210
|
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
this.#stopped.add(controller)
|
|
3819
|
-
controller.abort()
|
|
3820
|
-
}
|
|
3821
|
-
return this.#queue(this.#stop.bind(this))
|
|
4211
|
+
get status(): ApplicationServerInterface['status'] {
|
|
4212
|
+
return this.#server.status
|
|
3822
4213
|
}
|
|
3823
4214
|
|
|
3824
|
-
|
|
3825
|
-
const
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
)
|
|
3830
|
-
return queued
|
|
4215
|
+
get url(): string | undefined {
|
|
4216
|
+
const port = this.port
|
|
4217
|
+
if (port === undefined) return undefined
|
|
4218
|
+
const hostname = this.host.includes(':') ? \`[\${this.host}]\` : this.host
|
|
4219
|
+
return \`http://\${hostname}:\${port}\`
|
|
3831
4220
|
}
|
|
3832
4221
|
|
|
3833
|
-
async
|
|
3834
|
-
if (this.listening) return
|
|
3835
|
-
if (controller.signal.aborted) {
|
|
3836
|
-
if (this.#stopped.has(controller)) return
|
|
3837
|
-
throw new ApplicationServerError('LIFECYCLE', 'Application server startup was cancelled')
|
|
3838
|
-
}
|
|
4222
|
+
async start(signal?: AbortSignal): Promise<void> {
|
|
3839
4223
|
try {
|
|
3840
|
-
|
|
3841
|
-
throw new ApplicationServerError('LIFECYCLE', 'Application server startup was cancelled', {
|
|
3842
|
-
cause: signal.reason,
|
|
3843
|
-
})
|
|
3844
|
-
}
|
|
3845
|
-
await this.#listen(controller, signal)
|
|
4224
|
+
await this.#server.start(signal)
|
|
3846
4225
|
} catch (error) {
|
|
3847
|
-
|
|
3848
|
-
if (isApplicationServerError(error)) throw error
|
|
3849
|
-
throw new ApplicationServerError('LIFECYCLE', 'Failed to inspect startup signal', {
|
|
4226
|
+
throw new ApplicationServerError('LIFECYCLE', 'Failed to start application server', {
|
|
3850
4227
|
cause: error,
|
|
3851
4228
|
})
|
|
3852
4229
|
}
|
|
3853
4230
|
}
|
|
3854
4231
|
|
|
3855
|
-
async
|
|
3856
|
-
if (!this.listening) return
|
|
3857
|
-
await this.#close()
|
|
3858
|
-
}
|
|
3859
|
-
|
|
3860
|
-
async #listen(controller: AbortController, signal?: AbortSignal): Promise<void> {
|
|
3861
|
-
const relay = signal === undefined ? undefined : this.#abort.bind(this, controller, signal)
|
|
3862
|
-
const timer = setTimeout(this.#expire.bind(this, controller), this.#timeout)
|
|
4232
|
+
async stop(): Promise<void> {
|
|
3863
4233
|
try {
|
|
3864
|
-
|
|
3865
|
-
signal.addEventListener('abort', relay, { once: true })
|
|
3866
|
-
}
|
|
3867
|
-
this.#server.listen({
|
|
3868
|
-
port: this.#requestedPort,
|
|
3869
|
-
host: this.host,
|
|
3870
|
-
signal: controller.signal,
|
|
3871
|
-
})
|
|
3872
|
-
await once(this.#server, 'listening', { signal: controller.signal })
|
|
4234
|
+
await this.#server.stop()
|
|
3873
4235
|
} catch (error) {
|
|
3874
|
-
throw new ApplicationServerError('LIFECYCLE', 'Failed to
|
|
4236
|
+
throw new ApplicationServerError('LIFECYCLE', 'Failed to stop application server', {
|
|
3875
4237
|
cause: error,
|
|
3876
4238
|
})
|
|
3877
|
-
} finally {
|
|
3878
|
-
clearTimeout(timer)
|
|
3879
|
-
if (relay !== undefined && signal !== undefined) {
|
|
3880
|
-
signal.removeEventListener('abort', relay)
|
|
3881
|
-
}
|
|
3882
4239
|
}
|
|
3883
|
-
const address = this.#server.address()
|
|
3884
|
-
if (address === null || typeof address === 'string') {
|
|
3885
|
-
await this.#close()
|
|
3886
|
-
throw new ApplicationServerError('LIFECYCLE', 'Server did not expose a TCP address')
|
|
3887
|
-
}
|
|
3888
|
-
this.#port = address.port
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
|
-
#abort(controller: AbortController, signal: AbortSignal): void {
|
|
3892
|
-
controller.abort(signal.reason)
|
|
3893
|
-
}
|
|
3894
|
-
|
|
3895
|
-
#expire(controller: AbortController): void {
|
|
3896
|
-
controller.abort(new Error(\`Application server startup exceeded \${this.#timeout} milliseconds\`))
|
|
3897
|
-
}
|
|
3898
|
-
|
|
3899
|
-
#settle(controller: AbortController): void {
|
|
3900
|
-
this.#starts.delete(controller)
|
|
3901
4240
|
}
|
|
3902
4241
|
|
|
3903
|
-
async
|
|
4242
|
+
async destroy(): Promise<void> {
|
|
3904
4243
|
try {
|
|
3905
|
-
|
|
3906
|
-
this.#server.closeIdleConnections()
|
|
3907
|
-
this.#server.closeAllConnections()
|
|
3908
|
-
await closed
|
|
4244
|
+
await this.#server.destroy()
|
|
3909
4245
|
} catch (error) {
|
|
3910
|
-
throw new ApplicationServerError('LIFECYCLE', 'Failed to
|
|
4246
|
+
throw new ApplicationServerError('LIFECYCLE', 'Failed to destroy application server', {
|
|
3911
4247
|
cause: error,
|
|
3912
4248
|
})
|
|
4249
|
+
} finally {
|
|
4250
|
+
this.#dispatcher.destroy()
|
|
3913
4251
|
}
|
|
3914
4252
|
}
|
|
3915
4253
|
}
|
|
@@ -3932,16 +4270,17 @@ import { ApplicationServerRunner } from './ApplicationServerRunner.js'
|
|
|
3932
4270
|
/**
|
|
3933
4271
|
* Create a stopped application server.
|
|
3934
4272
|
*
|
|
3935
|
-
* @param options - Optional
|
|
4273
|
+
* @param options - Optional grouped server overrides.
|
|
3936
4274
|
* @returns A lifecycle-safe application server.
|
|
3937
4275
|
*
|
|
3938
4276
|
* @example
|
|
3939
4277
|
* \`\`\`ts
|
|
3940
4278
|
* import { createApplicationServer } from '@app/server'
|
|
3941
4279
|
*
|
|
3942
|
-
* ${CONST_KEYWORD} server = createApplicationServer({ port: 0 })
|
|
4280
|
+
* ${CONST_KEYWORD} server = createApplicationServer({ server: { port: 0 } })
|
|
3943
4281
|
* await server.start()
|
|
3944
4282
|
* await server.stop()
|
|
4283
|
+
* await server.destroy()
|
|
3945
4284
|
* \`\`\`
|
|
3946
4285
|
*/
|
|
3947
4286
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationServer(
|
|
@@ -3953,21 +4292,21 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationServer(
|
|
|
3953
4292
|
/**
|
|
3954
4293
|
* Start a process-owned application server.
|
|
3955
4294
|
*
|
|
3956
|
-
* @param options - Optional
|
|
4295
|
+
* @param options - Optional grouped server overrides.
|
|
3957
4296
|
* @returns The runner that owns signals and provides explicit asynchronous cleanup.
|
|
3958
4297
|
*
|
|
3959
4298
|
* @example
|
|
3960
4299
|
* \`\`\`ts
|
|
3961
4300
|
* import { startApplicationServer } from '@app/server'
|
|
3962
4301
|
*
|
|
3963
|
-
* ${CONST_KEYWORD} runner = startApplicationServer({ port: 0 })
|
|
4302
|
+
* ${CONST_KEYWORD} runner = startApplicationServer({ server: { port: 0 } })
|
|
3964
4303
|
* await runner.stop()
|
|
3965
4304
|
* \`\`\`
|
|
3966
4305
|
*/
|
|
3967
4306
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationServer(
|
|
3968
4307
|
options: ApplicationServerOptions = {},
|
|
3969
4308
|
): ApplicationServerRunnerInterface {
|
|
3970
|
-
const runner = new ApplicationServerRunner(options)
|
|
4309
|
+
const runner = new ApplicationServerRunner(new ApplicationServer(options))
|
|
3971
4310
|
runner.start()
|
|
3972
4311
|
return runner
|
|
3973
4312
|
}
|
|
@@ -3978,57 +4317,144 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationServer(
|
|
|
3978
4317
|
name: "appServerRunner",
|
|
3979
4318
|
summary: "The application server process lifecycle owner.",
|
|
3980
4319
|
category: "source",
|
|
3981
|
-
placeholders: Object.freeze([
|
|
3982
|
-
|
|
4320
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4321
|
+
name: "nameImport",
|
|
4322
|
+
description: "The selected layer import for APP_NAME."
|
|
4323
|
+
})]),
|
|
4324
|
+
content: `import type { EmitterInterface } from '@orkestrel/emitter'
|
|
4325
|
+
import type {
|
|
3983
4326
|
ApplicationServerInterface,
|
|
3984
|
-
|
|
4327
|
+
ApplicationServerRunnerEventMap,
|
|
3985
4328
|
ApplicationServerRunnerInterface,
|
|
4329
|
+
ApplicationServerRunnerOptions,
|
|
3986
4330
|
} from './types.js'
|
|
3987
|
-
import {
|
|
4331
|
+
import { Emitter } from '@orkestrel/emitter'
|
|
4332
|
+
{{nameImport}}
|
|
4333
|
+
import { ApplicationServerError } from './errors.js'
|
|
3988
4334
|
import { reportApplicationServerError } from './handlers.js'
|
|
3989
4335
|
|
|
3990
4336
|
/** Own process signals and startup failure handling for one application server. */
|
|
3991
4337
|
${EXPORT_KEYWORD} class ApplicationServerRunner implements ApplicationServerRunnerInterface {
|
|
4338
|
+
readonly #emitter: Emitter<ApplicationServerRunnerEventMap>
|
|
3992
4339
|
readonly #server: ApplicationServerInterface
|
|
3993
|
-
readonly #
|
|
4340
|
+
readonly #handler: () => void
|
|
4341
|
+
#controller: AbortController | undefined
|
|
4342
|
+
#queue: Promise<void> = Promise.resolve()
|
|
4343
|
+
#stopping: Promise<void> | undefined
|
|
4344
|
+
#announcement: number | undefined
|
|
3994
4345
|
#generation = 0
|
|
3995
4346
|
#started = false
|
|
3996
4347
|
|
|
3997
|
-
constructor(options:
|
|
3998
|
-
this.#
|
|
3999
|
-
|
|
4348
|
+
constructor(server: ApplicationServerInterface, options: ApplicationServerRunnerOptions = {}) {
|
|
4349
|
+
this.#emitter = new Emitter({
|
|
4350
|
+
...(options.on === undefined ? {} : { on: options.on }),
|
|
4351
|
+
...(options.error === undefined ? {} : { error: options.error }),
|
|
4352
|
+
})
|
|
4353
|
+
this.#server = server
|
|
4354
|
+
this.#handler = this.#shutdown.bind(this)
|
|
4355
|
+
this.#emitter.on('ready', this.#announce.bind(this))
|
|
4356
|
+
this.#emitter.on('fail', reportApplicationServerError)
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
get emitter(): EmitterInterface<ApplicationServerRunnerEventMap> {
|
|
4360
|
+
return this.#emitter
|
|
4000
4361
|
}
|
|
4001
4362
|
|
|
4002
4363
|
start(): void {
|
|
4003
4364
|
if (this.#started) return
|
|
4004
4365
|
this.#started = true
|
|
4366
|
+
this.#stopping = undefined
|
|
4005
4367
|
const generation = ++this.#generation
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4368
|
+
const controller = new AbortController()
|
|
4369
|
+
this.#controller = controller
|
|
4370
|
+
process.once('SIGINT', this.#handler)
|
|
4371
|
+
process.once('SIGTERM', this.#handler)
|
|
4372
|
+
this.#queue = this.#queue.then(this.#begin.bind(this, generation, controller))
|
|
4009
4373
|
}
|
|
4010
4374
|
|
|
4011
4375
|
stop(): Promise<void> {
|
|
4012
|
-
this.#
|
|
4376
|
+
if (this.#stopping !== undefined) return this.#stopping
|
|
4377
|
+
// Aborting first lets an in-flight substrate startup settle before stop inspects its state,
|
|
4378
|
+
// and the generation bump is what discards that abort's rejection while leaving a genuine
|
|
4379
|
+
// stop failure reportable. The shared queue then keeps a subsequent restart behind this complete
|
|
4380
|
+
// shutdown; concurrent callers join one substrate stop through #stopping, and the trailing catch
|
|
4381
|
+
// discards nothing — it only keeps one failed stop from wedging every later one.
|
|
4382
|
+
++this.#generation
|
|
4383
|
+
this.#controller?.abort()
|
|
4013
4384
|
this.#release()
|
|
4014
|
-
|
|
4385
|
+
const stopping = this.#queue.then(() => this.#server.stop())
|
|
4386
|
+
this.#stopping = stopping
|
|
4387
|
+
this.#queue = stopping.catch(() => undefined)
|
|
4388
|
+
void stopping.then(this.#finishStop.bind(this, stopping), this.#rejectStop.bind(this, stopping))
|
|
4389
|
+
return stopping
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4392
|
+
async #begin(generation: number, controller: AbortController): Promise<void> {
|
|
4393
|
+
if (generation !== this.#generation || !this.#started) return
|
|
4394
|
+
try {
|
|
4395
|
+
await this.#server.start(controller.signal)
|
|
4396
|
+
await this.#ready(generation)
|
|
4397
|
+
} catch (error) {
|
|
4398
|
+
this.#fail(generation, error)
|
|
4399
|
+
}
|
|
4015
4400
|
}
|
|
4016
4401
|
|
|
4017
4402
|
#fail(generation: number, error: unknown): void {
|
|
4018
4403
|
if (generation !== this.#generation) return
|
|
4019
4404
|
this.#release()
|
|
4020
|
-
|
|
4405
|
+
this.#emitter.emit('fail', error)
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
#finishStop(stopping: Promise<void>): void {
|
|
4409
|
+
if (this.#stopping === stopping) this.#stopping = undefined
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
#rejectStop(stopping: Promise<void>, error: unknown): void {
|
|
4413
|
+
this.#finishStop(stopping)
|
|
4414
|
+
this.#emitter.emit('fail', error)
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
async #ready(generation: number): Promise<void> {
|
|
4418
|
+
if (generation !== this.#generation || !this.#started) return
|
|
4419
|
+
const url = this.#server.url
|
|
4420
|
+
if (url === undefined) {
|
|
4421
|
+
const failure = new ApplicationServerError(
|
|
4422
|
+
'LIFECYCLE',
|
|
4423
|
+
'Application server did not expose a URL after successful startup',
|
|
4424
|
+
)
|
|
4425
|
+
try {
|
|
4426
|
+
await this.#server.stop()
|
|
4427
|
+
} catch (error) {
|
|
4428
|
+
this.#fail(generation, error)
|
|
4429
|
+
return
|
|
4430
|
+
}
|
|
4431
|
+
this.#fail(generation, failure)
|
|
4432
|
+
return
|
|
4433
|
+
}
|
|
4434
|
+
this.#announcement = generation
|
|
4435
|
+
this.#emitter.emit('ready', url)
|
|
4436
|
+
this.#announcement = undefined
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
#announce(url: string): void {
|
|
4440
|
+
if (this.#announcement !== this.#generation) return
|
|
4441
|
+
try {
|
|
4442
|
+
process.stderr.write(\`[READY] \${APP_NAME} \${url}\\n\`)
|
|
4443
|
+
} catch (error) {
|
|
4444
|
+
const stopped = this.stop()
|
|
4445
|
+
const generation = this.#generation
|
|
4446
|
+
void stopped.then(this.#fail.bind(this, generation, error), () => undefined)
|
|
4447
|
+
}
|
|
4021
4448
|
}
|
|
4022
4449
|
|
|
4023
4450
|
#shutdown(): void {
|
|
4024
4451
|
const stopped = this.stop()
|
|
4025
|
-
|
|
4026
|
-
void stopped.catch(this.#fail.bind(this, generation))
|
|
4452
|
+
void stopped.catch(() => undefined)
|
|
4027
4453
|
}
|
|
4028
4454
|
|
|
4029
4455
|
#release(): void {
|
|
4030
|
-
process.off('SIGINT', this.#
|
|
4031
|
-
process.off('SIGTERM', this.#
|
|
4456
|
+
process.off('SIGINT', this.#handler)
|
|
4457
|
+
process.off('SIGTERM', this.#handler)
|
|
4032
4458
|
this.#started = false
|
|
4033
4459
|
}
|
|
4034
4460
|
}
|
|
@@ -4044,6 +4470,7 @@ ${EXPORT_KEYWORD} class ApplicationServerRunner implements ApplicationServerRunn
|
|
|
4044
4470
|
export * from './constants.js'
|
|
4045
4471
|
export * from './errors.js'
|
|
4046
4472
|
export * from './parsers.js'
|
|
4473
|
+
export * from './routes.js'
|
|
4047
4474
|
export * from './handlers.js'
|
|
4048
4475
|
export * from './ApplicationServer.js'
|
|
4049
4476
|
export * from './ApplicationServerRunner.js'
|
|
@@ -4068,10 +4495,16 @@ try {
|
|
|
4068
4495
|
setup: Object.freeze({
|
|
4069
4496
|
id: "setup",
|
|
4070
4497
|
name: "setup",
|
|
4071
|
-
summary: "The generated-minimal `tests/setup.ts`
|
|
4498
|
+
summary: "The generated-minimal `tests/setup.ts` shared test helpers.",
|
|
4072
4499
|
category: "tests",
|
|
4073
|
-
placeholders: Object.freeze([
|
|
4074
|
-
|
|
4500
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4501
|
+
name: "eventImport",
|
|
4502
|
+
description: "The optional emitter types used by application server tests."
|
|
4503
|
+
}), Object.freeze({
|
|
4504
|
+
name: "eventHelper",
|
|
4505
|
+
description: "The optional typed event waiter used by application server tests."
|
|
4506
|
+
})]),
|
|
4507
|
+
content: `{{eventImport}}// ── Call recorder (a real callback, not a mock) ──────────────────────────────
|
|
4075
4508
|
//
|
|
4076
4509
|
// The test rules require a recording callback when a test only needs to count calls or inspect arguments:
|
|
4077
4510
|
// recorder — a real listener that records every invocation — rather than a test-
|
|
@@ -4111,12 +4544,145 @@ ${EXPORT_KEYWORD} function createRecorder<TArgs extends readonly unknown[]>(): T
|
|
|
4111
4544
|
},
|
|
4112
4545
|
}
|
|
4113
4546
|
}
|
|
4114
|
-
|
|
4547
|
+
{{eventHelper}}
|
|
4115
4548
|
/** Whether a repository-relative Vue SFC belongs to the private browser application. */
|
|
4116
4549
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserVuePath(path: string): boolean {
|
|
4117
4550
|
const normalized = path.replaceAll('\\\\', '/')
|
|
4118
4551
|
return normalized.startsWith('app/browser/')
|
|
4119
4552
|
}
|
|
4553
|
+
`
|
|
4554
|
+
}),
|
|
4555
|
+
configTest: Object.freeze({
|
|
4556
|
+
id: "configTest",
|
|
4557
|
+
name: "configTest",
|
|
4558
|
+
summary: "The generated root Vite configuration behavior test.",
|
|
4559
|
+
category: "tests",
|
|
4560
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4561
|
+
name: "imports",
|
|
4562
|
+
description: "The machinery-aware root configuration imports."
|
|
4563
|
+
}), Object.freeze({
|
|
4564
|
+
name: "cases",
|
|
4565
|
+
description: "The machinery-aware root configuration cases."
|
|
4566
|
+
})]),
|
|
4567
|
+
content: `{{imports}}
|
|
4568
|
+
|
|
4569
|
+
describe('root Vite configuration', () => {
|
|
4570
|
+
it('keeps workspace paths physically contained', () => {
|
|
4571
|
+
const root = resolveWorkspacePath('.')
|
|
4572
|
+
const source = resolveWorkspacePath('src')
|
|
4573
|
+
const parent = resolveWorkspacePath('..')
|
|
4574
|
+
|
|
4575
|
+
expect(workspacePath(root)).toBe('')
|
|
4576
|
+
expect(workspacePath(source)).toBe('src')
|
|
4577
|
+
expect(workspacePath(parent)).toBeUndefined()
|
|
4578
|
+
expect(containedPath(root, source)).toBe(true)
|
|
4579
|
+
expect(containedPath(root, parent)).toBe(false)
|
|
4580
|
+
})
|
|
4581
|
+
|
|
4582
|
+
it('enforces environment direction for paths and module sources', () => {
|
|
4583
|
+
expect(environmentPathError('src/core', 'app/core/index.ts')).toBe(
|
|
4584
|
+
'Published modules cannot depend on private application modules',
|
|
4585
|
+
)
|
|
4586
|
+
expect(environmentPathError('app/core', 'src/browser/index.ts')).toBe(
|
|
4587
|
+
'Core modules must remain host-independent',
|
|
4588
|
+
)
|
|
4589
|
+
expect(environmentPathError('app/browser', 'src/server/index.ts')).toBe(
|
|
4590
|
+
'Browser modules cannot depend on Node or server-only modules',
|
|
4591
|
+
)
|
|
4592
|
+
expect(environmentPathError('app/server', 'src/browser/index.ts')).toBe(
|
|
4593
|
+
'Server modules cannot depend on Vue or browser-only modules',
|
|
4594
|
+
)
|
|
4595
|
+
expect(environmentPathError('app/browser', 'src/core/index.ts')).toBeUndefined()
|
|
4596
|
+
expect(environmentSourceError('src/core', 'node:path')).toBe(
|
|
4597
|
+
'Core modules must remain host-independent',
|
|
4598
|
+
)
|
|
4599
|
+
expect(environmentSourceError('app/server', 'vue')).toBe(
|
|
4600
|
+
'Server modules cannot depend on Vue or browser-only modules',
|
|
4601
|
+
)
|
|
4602
|
+
expect(environmentSourceError('app/browser', '@app/core')).toBeUndefined()
|
|
4603
|
+
}){{cases}}
|
|
4604
|
+
})
|
|
4605
|
+
`
|
|
4606
|
+
}),
|
|
4607
|
+
serviceConformance: Object.freeze({
|
|
4608
|
+
id: "serviceConformance",
|
|
4609
|
+
name: "serviceConformance",
|
|
4610
|
+
summary: "The generated service-vendor structure and configuration conformance test.",
|
|
4611
|
+
category: "tests",
|
|
4612
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4613
|
+
name: "services",
|
|
4614
|
+
description: "The sorted service vendor directory names."
|
|
4615
|
+
})]),
|
|
4616
|
+
content: `import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
|
|
4617
|
+
import { join } from 'node:path'
|
|
4618
|
+
import { isRecord, parseJSON } from '@orkestrel/contract'
|
|
4619
|
+
import { expect, it } from 'vitest'
|
|
4620
|
+
|
|
4621
|
+
it('keeps every service vendor structurally complete and exactly configured', () => {
|
|
4622
|
+
const declared = {{services}}
|
|
4623
|
+
const root = join(process.cwd(), 'tests', 'service')
|
|
4624
|
+
const directories = readdirSync(root, { withFileTypes: true })
|
|
4625
|
+
.filter((entry) => entry.isDirectory())
|
|
4626
|
+
.map((entry) => entry.name)
|
|
4627
|
+
.sort()
|
|
4628
|
+
const manifest = parseJSON(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
|
|
4629
|
+
const scripts = isRecord(manifest) && isRecord(manifest.scripts) ? manifest.scripts : {}
|
|
4630
|
+
const config = readFileSync(join(process.cwd(), 'vite.config.ts'), 'utf8')
|
|
4631
|
+
const provisioner = join(process.cwd(), 'scripts', 'service.sh')
|
|
4632
|
+
|
|
4633
|
+
expect(directories).toEqual(declared)
|
|
4634
|
+
if (!existsSync(provisioner) || !lstatSync(provisioner).isFile()) {
|
|
4635
|
+
throw new Error(
|
|
4636
|
+
'Service vendors require scripts/service.sh to provision every declared vendor idempotently or exit nonzero',
|
|
4637
|
+
)
|
|
4638
|
+
}
|
|
4639
|
+
for (const service of declared) {
|
|
4640
|
+
const setup = join(root, service, 'setup.ts')
|
|
4641
|
+
if (!existsSync(setup) || !lstatSync(setup).isFile()) {
|
|
4642
|
+
throw new Error(
|
|
4643
|
+
\`Service vendor "\${service}" is missing tests/service/\${service}/setup.ts; add a readiness module that probes and warms the vendor and throws when unavailable\`,
|
|
4644
|
+
)
|
|
4645
|
+
}
|
|
4646
|
+
const label = \`name: { label: 'service:\${service}', color: 'red' }\`
|
|
4647
|
+
const include = \`include: ['tests/service/\${service}/**/*.test.ts']\`
|
|
4648
|
+
const setupFiles = \`setupFiles: ['./tests/setup.ts', './tests/setupServer.ts', './tests/service/\${service}/setup.ts']\`
|
|
4649
|
+
if (!config.includes(label) || !config.includes(include) || !config.includes(setupFiles)) {
|
|
4650
|
+
throw new Error(\`Service vendor "\${service}" is missing its exact Vite project\`)
|
|
4651
|
+
}
|
|
4652
|
+
const command = \`vitest run --config vite.config.ts --no-cache --reporter=dot --project service:\${service}\`
|
|
4653
|
+
if (scripts[\`test:service:\${service}\`] !== command) {
|
|
4654
|
+
throw new Error(\`Service vendor "\${service}" is missing its exact npm script\`)
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
const aggregate =
|
|
4658
|
+
'vitest run --config vite.config.ts --no-cache --reporter=dot ' +
|
|
4659
|
+
declared.map((service) => \`--project service:\${service}\`).join(' ')
|
|
4660
|
+
if (scripts['test:service'] !== aggregate) {
|
|
4661
|
+
throw new Error('test:service must name every declared service exactly once')
|
|
4662
|
+
}
|
|
4663
|
+
if (typeof scripts.test !== 'string' || scripts.test.includes('test:service')) {
|
|
4664
|
+
throw new Error('The default test script must omit live service proofs')
|
|
4665
|
+
}
|
|
4666
|
+
if (
|
|
4667
|
+
typeof scripts.prepublishOnly !== 'string' ||
|
|
4668
|
+
!scripts.prepublishOnly.endsWith(' && npm run test:service')
|
|
4669
|
+
) {
|
|
4670
|
+
throw new Error('prepublishOnly must end with the aggregate live service proof')
|
|
4671
|
+
}
|
|
4672
|
+
})
|
|
4673
|
+
`
|
|
4674
|
+
}),
|
|
4675
|
+
serviceProvisioner: Object.freeze({
|
|
4676
|
+
id: "serviceProvisioner",
|
|
4677
|
+
name: "serviceProvisioner",
|
|
4678
|
+
summary: "The birth-only workspace-owned service-vendor provisioner skeleton.",
|
|
4679
|
+
category: "orchestration",
|
|
4680
|
+
placeholders: Object.freeze([]),
|
|
4681
|
+
content: `#!/bin/sh
|
|
4682
|
+
set -eu
|
|
4683
|
+
|
|
4684
|
+
printf '%s\\n' 'Service provisioning is not configured. Implement scripts/service.sh to provision every declared vendor idempotently, then remove this failure.' >&2
|
|
4685
|
+
exit 1
|
|
4120
4686
|
`
|
|
4121
4687
|
}),
|
|
4122
4688
|
policyTest: Object.freeze({
|
|
@@ -4124,32 +4690,25 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserVuePath(path: string): boolean {
|
|
|
4124
4690
|
name: "policyTest",
|
|
4125
4691
|
summary: "The generated repository filename-policy test.",
|
|
4126
4692
|
category: "tests",
|
|
4127
|
-
placeholders: Object.freeze([
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
}),
|
|
4136
|
-
Object.freeze({
|
|
4137
|
-
name: "browserPolicyTest",
|
|
4138
|
-
description: "The optional capability-gated Chromium policy test."
|
|
4139
|
-
}),
|
|
4140
|
-
Object.freeze({
|
|
4141
|
-
name: "vuePolicyImport",
|
|
4142
|
-
description: "The optional official Vue SFC compiler import."
|
|
4143
|
-
}),
|
|
4144
|
-
Object.freeze({
|
|
4145
|
-
name: "workspacePolicyAssertion",
|
|
4146
|
-
description: "The formatter-stable workspace policy assertion."
|
|
4147
|
-
})
|
|
4148
|
-
]),
|
|
4149
|
-
content: `import { globSync{{browserPolicySpecifier}} } from 'node:fs'
|
|
4693
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4694
|
+
name: "vuePolicyImport",
|
|
4695
|
+
description: "The Vue SFC parser import, when the workspace declares app/browser."
|
|
4696
|
+
}), Object.freeze({
|
|
4697
|
+
name: "workspacePolicyAssertion",
|
|
4698
|
+
description: "The workspace coding-law assertion, carrying a Vue script extractor when the workspace declares app/browser."
|
|
4699
|
+
})]),
|
|
4700
|
+
content: `import { globSync } from 'node:fs'
|
|
4150
4701
|
import { describe, expect, it } from 'vitest'
|
|
4151
4702
|
import { isBrowserVuePath } from './setup.js'
|
|
4152
|
-
import { inspectCodingWorkspace } from './setupPolicy.js'{{
|
|
4703
|
+
import { inspectCodingLaw, inspectCodingWorkspace, isFunctionDomainPath } from './setupPolicy.js'{{vuePolicyImport}}
|
|
4704
|
+
|
|
4705
|
+
${CONST_KEYWORD} FUNCTION_MODULE_PATH = 'app/browser/composables/useTheme.ts'
|
|
4706
|
+
${CONST_KEYWORD} FUNCTION_MODULE_VIOLATION =
|
|
4707
|
+
FUNCTION_MODULE_PATH + ' declarations do not form one matching exported function implementation'
|
|
4708
|
+
${CONST_KEYWORD} FUNCTION_DOMAIN_FILE_PATH = 'app/server/composables.ts'
|
|
4709
|
+
${CONST_KEYWORD} FUNCTION_DOMAIN_FILE_VIOLATION =
|
|
4710
|
+
FUNCTION_DOMAIN_FILE_PATH +
|
|
4711
|
+
' names a function domain, which belongs in a folder rather than a file'
|
|
4153
4712
|
|
|
4154
4713
|
describe('repository coding law', () => {
|
|
4155
4714
|
it('keeps Vue single-file components exclusively in browser environments', () => {
|
|
@@ -4160,7 +4719,126 @@ describe('repository coding law', () => {
|
|
|
4160
4719
|
|
|
4161
4720
|
it('enforces source placement, exports, readonly contracts, and syntax law', () => {
|
|
4162
4721
|
{{workspacePolicyAssertion}}
|
|
4163
|
-
})
|
|
4722
|
+
})
|
|
4723
|
+
|
|
4724
|
+
it('accepts one matching exported function in a registered domain', () => {
|
|
4725
|
+
expect(
|
|
4726
|
+
inspectCodingLaw(
|
|
4727
|
+
FUNCTION_MODULE_PATH,
|
|
4728
|
+
"import type { Ref } from 'vue'\\nexport function useTheme(): Ref<undefined> { throw new Error() }",
|
|
4729
|
+
),
|
|
4730
|
+
).toEqual([])
|
|
4731
|
+
})
|
|
4732
|
+
|
|
4733
|
+
it('accepts one matching exported generator in a registered domain', () => {
|
|
4734
|
+
expect(
|
|
4735
|
+
inspectCodingLaw(FUNCTION_MODULE_PATH, 'export function* useTheme(): Generator<void> {}'),
|
|
4736
|
+
).toEqual([])
|
|
4737
|
+
})
|
|
4738
|
+
|
|
4739
|
+
it('accepts overload signatures beside one matching implementation', () => {
|
|
4740
|
+
expect(
|
|
4741
|
+
inspectCodingLaw(
|
|
4742
|
+
FUNCTION_MODULE_PATH,
|
|
4743
|
+
'export function useTheme(): void\\nexport function useTheme(mode: string): void\\nexport function useTheme(_mode?: string): void {}',
|
|
4744
|
+
),
|
|
4745
|
+
).toEqual([])
|
|
4746
|
+
})
|
|
4747
|
+
|
|
4748
|
+
it('rejects a bodyless function-domain declaration', () => {
|
|
4749
|
+
expect(
|
|
4750
|
+
inspectCodingLaw(FUNCTION_MODULE_PATH, 'export declare function useTheme(): void'),
|
|
4751
|
+
).toEqual([FUNCTION_MODULE_VIOLATION])
|
|
4752
|
+
})
|
|
4753
|
+
|
|
4754
|
+
it('rejects a mismatched bodyless declaration beside one implementation', () => {
|
|
4755
|
+
expect(
|
|
4756
|
+
inspectCodingLaw(
|
|
4757
|
+
FUNCTION_MODULE_PATH,
|
|
4758
|
+
'declare function smuggled(secret: string): void\\nexport function useTheme(): void {}',
|
|
4759
|
+
),
|
|
4760
|
+
).toEqual([FUNCTION_MODULE_VIOLATION])
|
|
4761
|
+
})
|
|
4762
|
+
|
|
4763
|
+
it('rejects two exported functions in a function module', () => {
|
|
4764
|
+
expect(
|
|
4765
|
+
inspectCodingLaw(
|
|
4766
|
+
FUNCTION_MODULE_PATH,
|
|
4767
|
+
'export function useTheme(): void {}\\nexport function useMode(): void {}',
|
|
4768
|
+
),
|
|
4769
|
+
).toEqual([FUNCTION_MODULE_VIOLATION])
|
|
4770
|
+
})
|
|
4771
|
+
|
|
4772
|
+
it('rejects module data beside a function-domain export', () => {
|
|
4773
|
+
expect(
|
|
4774
|
+
inspectCodingLaw(
|
|
4775
|
+
FUNCTION_MODULE_PATH,
|
|
4776
|
+
"const THEME = 'dark'\\nexport function useTheme(): string { return THEME }",
|
|
4777
|
+
),
|
|
4778
|
+
).toEqual([FUNCTION_MODULE_VIOLATION])
|
|
4779
|
+
})
|
|
4780
|
+
|
|
4781
|
+
it('rejects a function whose name differs from its filename', () => {
|
|
4782
|
+
expect(inspectCodingLaw(FUNCTION_MODULE_PATH, 'export function useMode(): void {}')).toEqual([
|
|
4783
|
+
FUNCTION_MODULE_VIOLATION,
|
|
4784
|
+
])
|
|
4785
|
+
})
|
|
4786
|
+
|
|
4787
|
+
it('rejects a non-exported function-domain declaration', () => {
|
|
4788
|
+
expect(inspectCodingLaw(FUNCTION_MODULE_PATH, 'function useTheme(): void {}')).toEqual([
|
|
4789
|
+
FUNCTION_MODULE_VIOLATION,
|
|
4790
|
+
])
|
|
4791
|
+
})
|
|
4792
|
+
|
|
4793
|
+
it('rejects a default function-domain export', () => {
|
|
4794
|
+
expect(
|
|
4795
|
+
inspectCodingLaw(FUNCTION_MODULE_PATH, 'export default function useTheme(): void {}'),
|
|
4796
|
+
).toEqual([FUNCTION_MODULE_VIOLATION])
|
|
4797
|
+
})
|
|
4798
|
+
|
|
4799
|
+
it('keeps index modules outside the function-domain shape', () => {
|
|
4800
|
+
const path = 'app/browser/composables/index.ts'
|
|
4801
|
+
|
|
4802
|
+
expect(isFunctionDomainPath(path)).toBe(false)
|
|
4803
|
+
expect(inspectCodingLaw(path, 'export function index(): void {}')).toContain(
|
|
4804
|
+
path + ' places module functions in their centralized kind file',
|
|
4805
|
+
)
|
|
4806
|
+
})
|
|
4807
|
+
|
|
4808
|
+
it('keeps main modules outside the function-domain shape', () => {
|
|
4809
|
+
const path = 'app/browser/composables/main.ts'
|
|
4810
|
+
|
|
4811
|
+
expect(isFunctionDomainPath(path)).toBe(false)
|
|
4812
|
+
expect(inspectCodingLaw(path, 'export function main(): void {}')).toContain(
|
|
4813
|
+
path + ' places module functions in their centralized kind file',
|
|
4814
|
+
)
|
|
4815
|
+
})
|
|
4816
|
+
|
|
4817
|
+
it('rejects a file named for a registered function domain', () => {
|
|
4818
|
+
const content =
|
|
4819
|
+
"import { parentPort } from 'node:worker_threads'\\nexport function start(): void { parentPort?.close() }"
|
|
4820
|
+
|
|
4821
|
+
expect(inspectCodingLaw(FUNCTION_DOMAIN_FILE_PATH, content)).toEqual([
|
|
4822
|
+
FUNCTION_DOMAIN_FILE_VIOLATION,
|
|
4823
|
+
])
|
|
4824
|
+
})
|
|
4825
|
+
|
|
4826
|
+
it('keeps camelCase modules in unregistered domains under centralized placement', () => {
|
|
4827
|
+
const path = 'app/browser/services/normalizePath.ts'
|
|
4828
|
+
|
|
4829
|
+
expect(isFunctionDomainPath(path)).toBe(false)
|
|
4830
|
+
expect(inspectCodingLaw(path, 'export function normalizePath(): void {}')).toEqual([
|
|
4831
|
+
path + ' places module functions in their centralized kind file',
|
|
4832
|
+
])
|
|
4833
|
+
})
|
|
4834
|
+
|
|
4835
|
+
it('preserves the self-contained Node runtime exemption', () => {
|
|
4836
|
+
const path = 'app/server/worker.ts'
|
|
4837
|
+
const content =
|
|
4838
|
+
"import { parentPort } from 'node:worker_threads'\\nconst port = parentPort\\nexport function start(): void { port?.close() }"
|
|
4839
|
+
|
|
4840
|
+
expect(inspectCodingLaw(path, content)).toEqual([])
|
|
4841
|
+
})
|
|
4164
4842
|
})
|
|
4165
4843
|
`
|
|
4166
4844
|
}),
|
|
@@ -4180,6 +4858,7 @@ ${IMPORT_KEYWORD} { createServer } from 'node:http'
|
|
|
4180
4858
|
/** One real application child process plus its captured diagnostic output. */
|
|
4181
4859
|
${EXPORT_KEYWORD} interface ApplicationProcessInterface {
|
|
4182
4860
|
readonly child: ChildProcess
|
|
4861
|
+
readonly ready: Promise<void>
|
|
4183
4862
|
output(): string
|
|
4184
4863
|
}
|
|
4185
4864
|
|
|
@@ -4219,11 +4898,17 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} buildApplicationServer(): void {
|
|
|
4219
4898
|
}
|
|
4220
4899
|
}
|
|
4221
4900
|
|
|
4222
|
-
/**
|
|
4223
|
-
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD}
|
|
4901
|
+
/** Start a real Node server bound to one loopback port, or 0 for an ephemeral one. */
|
|
4902
|
+
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} startLoopbackServer(port: number): Promise<Server> {
|
|
4224
4903
|
const server = createServer()
|
|
4225
|
-
server.listen(
|
|
4904
|
+
server.listen(port, '127.0.0.1')
|
|
4226
4905
|
await once(server, 'listening')
|
|
4906
|
+
return server
|
|
4907
|
+
}
|
|
4908
|
+
|
|
4909
|
+
/** Reserve and release a real loopback port for an immediate child-process bind. */
|
|
4910
|
+
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<number> {
|
|
4911
|
+
const server = await startLoopbackServer(0)
|
|
4227
4912
|
const address = server.address()
|
|
4228
4913
|
if (address === null || typeof address === 'string') {
|
|
4229
4914
|
await stopNodeServer(server)
|
|
@@ -4233,23 +4918,6 @@ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<numbe
|
|
|
4233
4918
|
return address.port
|
|
4234
4919
|
}
|
|
4235
4920
|
|
|
4236
|
-
/** Wait until one in-process application server responds on loopback. */
|
|
4237
|
-
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} waitForLoopbackResponse(port: number): Promise<Response> {
|
|
4238
|
-
const deadline = Date.now() + 10_000
|
|
4239
|
-
let failure: unknown
|
|
4240
|
-
while (Date.now() < deadline) {
|
|
4241
|
-
try {
|
|
4242
|
-
return await fetch(\`http://127.0.0.1:\${port}\`, {
|
|
4243
|
-
signal: AbortSignal.timeout(250),
|
|
4244
|
-
})
|
|
4245
|
-
} catch (error) {
|
|
4246
|
-
failure = error
|
|
4247
|
-
await new Promise<void>((resolvePromise) => setTimeout(resolvePromise, 25))
|
|
4248
|
-
}
|
|
4249
|
-
}
|
|
4250
|
-
throw new Error(\`application server did not become ready: \${String(failure)}\`)
|
|
4251
|
-
}
|
|
4252
|
-
|
|
4253
4921
|
/** Start the built application entry as a real child process. */
|
|
4254
4922
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationProcess(
|
|
4255
4923
|
port: number,
|
|
@@ -4266,16 +4934,20 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationProcess(
|
|
|
4266
4934
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4267
4935
|
})
|
|
4268
4936
|
const output: string[] = []
|
|
4937
|
+
const readiness = Promise.withResolvers<void>()
|
|
4269
4938
|
child.stdout?.setEncoding('utf8')
|
|
4270
4939
|
child.stderr?.setEncoding('utf8')
|
|
4271
4940
|
child.stdout?.on('data', (chunk: unknown) => {
|
|
4272
4941
|
if (typeof chunk === 'string') output.push(chunk)
|
|
4273
4942
|
})
|
|
4274
4943
|
child.stderr?.on('data', (chunk: unknown) => {
|
|
4275
|
-
if (typeof chunk
|
|
4944
|
+
if (typeof chunk !== 'string') return
|
|
4945
|
+
output.push(chunk)
|
|
4946
|
+
if (output.join('').includes('[READY] ')) readiness.resolve()
|
|
4276
4947
|
})
|
|
4277
4948
|
return {
|
|
4278
4949
|
child,
|
|
4950
|
+
ready: readiness.promise,
|
|
4279
4951
|
output() {
|
|
4280
4952
|
return output.join('')
|
|
4281
4953
|
},
|
|
@@ -4440,15 +5112,28 @@ describe('create{{pascal}}', () => {
|
|
|
4440
5112
|
name: "appCoreTest",
|
|
4441
5113
|
summary: "The host-independent application test.",
|
|
4442
5114
|
category: "tests",
|
|
4443
|
-
placeholders: Object.freeze([
|
|
5115
|
+
placeholders: Object.freeze([
|
|
5116
|
+
Object.freeze({
|
|
5117
|
+
name: "guardImport",
|
|
5118
|
+
description: "The optional shared-record guard test import."
|
|
5119
|
+
}),
|
|
5120
|
+
Object.freeze({
|
|
5121
|
+
name: "readImport",
|
|
5122
|
+
description: "The optional shared health-boundary read test import."
|
|
5123
|
+
}),
|
|
5124
|
+
Object.freeze({
|
|
5125
|
+
name: "boundary",
|
|
5126
|
+
description: "The optional shared health-boundary contract test."
|
|
5127
|
+
})
|
|
5128
|
+
]),
|
|
4444
5129
|
content: `import {
|
|
4445
5130
|
APP_NAME,
|
|
4446
5131
|
ApplicationError,
|
|
4447
5132
|
createApplication,
|
|
4448
5133
|
isApplicationError,
|
|
4449
|
-
MAX_APPLICATION_NAME_INPUT_LENGTH,
|
|
5134
|
+
{{guardImport}} MAX_APPLICATION_NAME_INPUT_LENGTH,
|
|
4450
5135
|
parseApplicationName,
|
|
4451
|
-
} from '@app/core'
|
|
5136
|
+
{{readImport}}} from '@app/core'
|
|
4452
5137
|
import { describe, expect, it } from 'vitest'
|
|
4453
5138
|
|
|
4454
5139
|
describe('createApplication', () => {
|
|
@@ -4485,7 +5170,14 @@ describe('createApplication', () => {
|
|
|
4485
5170
|
revocable.revoke()
|
|
4486
5171
|
expect(isApplicationError(revocable.proxy)).toBe(false)
|
|
4487
5172
|
})
|
|
4488
|
-
|
|
5173
|
+
|
|
5174
|
+
it('refuses a foreign application error', () => {
|
|
5175
|
+
const foreign = new Error('foreign')
|
|
5176
|
+
foreign.name = 'ApplicationError'
|
|
5177
|
+
|
|
5178
|
+
expect(isApplicationError(foreign)).toBe(false)
|
|
5179
|
+
})
|
|
5180
|
+
}){{boundary}}
|
|
4489
5181
|
`
|
|
4490
5182
|
}),
|
|
4491
5183
|
appBrowserTest: Object.freeze({
|
|
@@ -4493,18 +5185,36 @@ describe('createApplication', () => {
|
|
|
4493
5185
|
name: "appBrowserTest",
|
|
4494
5186
|
summary: "The real-browser application mount test.",
|
|
4495
5187
|
category: "tests",
|
|
4496
|
-
placeholders: Object.freeze([
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
5188
|
+
placeholders: Object.freeze([
|
|
5189
|
+
Object.freeze({
|
|
5190
|
+
name: "browserTestNameImport",
|
|
5191
|
+
description: "The layer-correct browser APP_NAME test import."
|
|
5192
|
+
}),
|
|
5193
|
+
Object.freeze({
|
|
5194
|
+
name: "showcaseImport",
|
|
5195
|
+
description: "The optional showcase factory test import."
|
|
5196
|
+
}),
|
|
5197
|
+
Object.freeze({
|
|
5198
|
+
name: "entryImport",
|
|
5199
|
+
description: "The optional seeder and boundary-entry test imports."
|
|
5200
|
+
}),
|
|
5201
|
+
Object.freeze({
|
|
5202
|
+
name: "showcase",
|
|
5203
|
+
description: "The optional real-browser showcase mount test."
|
|
5204
|
+
}),
|
|
5205
|
+
Object.freeze({
|
|
5206
|
+
name: "boundary",
|
|
5207
|
+
description: "The optional real-browser boundary degradation test."
|
|
5208
|
+
})
|
|
5209
|
+
]),
|
|
4500
5210
|
content: `{{browserTestNameImport}}
|
|
4501
5211
|
import {
|
|
4502
5212
|
BrowserApplicationError,
|
|
4503
5213
|
createBrowserApplication,
|
|
4504
|
-
isBrowserApplicationError,
|
|
5214
|
+
{{showcaseImport}} isBrowserApplicationError,
|
|
4505
5215
|
MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH,
|
|
4506
5216
|
parseBrowserApplicationOptions,
|
|
4507
|
-
} from '@app/browser'
|
|
5217
|
+
{{entryImport}}} from '@app/browser'
|
|
4508
5218
|
import { buildElement } from '../../setupBrowser.js'
|
|
4509
5219
|
import { describe, expect, it } from 'vitest'
|
|
4510
5220
|
|
|
@@ -4588,313 +5298,205 @@ describe('createBrowserApplication', () => {
|
|
|
4588
5298
|
expect(isBrowserApplicationError(new Error('plain'))).toBe(false)
|
|
4589
5299
|
expect(isBrowserApplicationError(revocable.proxy)).toBe(false)
|
|
4590
5300
|
})
|
|
4591
|
-
})
|
|
5301
|
+
}){{showcase}}{{boundary}}
|
|
4592
5302
|
`
|
|
4593
5303
|
}),
|
|
4594
5304
|
appServerTest: Object.freeze({
|
|
4595
5305
|
id: "appServerTest",
|
|
4596
5306
|
name: "appServerTest",
|
|
4597
|
-
summary: "The real
|
|
5307
|
+
summary: "The real dispatcher, server-substrate, and runner integration test.",
|
|
4598
5308
|
category: "tests",
|
|
4599
|
-
placeholders: Object.freeze([
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
5309
|
+
placeholders: Object.freeze([
|
|
5310
|
+
Object.freeze({
|
|
5311
|
+
name: "testNameImport",
|
|
5312
|
+
description: "The layer-correct APP_NAME and health-contract test import."
|
|
5313
|
+
}),
|
|
5314
|
+
Object.freeze({
|
|
5315
|
+
name: "serverImport",
|
|
5316
|
+
description: "The application server test import, minus any relocated health contract."
|
|
5317
|
+
}),
|
|
5318
|
+
Object.freeze({
|
|
5319
|
+
name: "boundary",
|
|
5320
|
+
description: "The optional real-server shared-boundary test."
|
|
5321
|
+
})
|
|
5322
|
+
]),
|
|
4603
5323
|
content: `{{testNameImport}}
|
|
4604
|
-
|
|
4605
|
-
APP_HEALTH_METHOD,
|
|
4606
|
-
APP_HEALTH_PATH,
|
|
4607
|
-
APP_MAX_CONNECTIONS,
|
|
4608
|
-
ApplicationServerRunner,
|
|
4609
|
-
createApplicationServer,
|
|
4610
|
-
startApplicationServer,
|
|
4611
|
-
} from '@app/server'
|
|
4612
|
-
import { once } from 'node:events'
|
|
4613
|
-
import { createServer } from 'node:http'
|
|
4614
|
-
import { connect } from 'node:net'
|
|
5324
|
+
{{serverImport}}
|
|
4615
5325
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
4616
5326
|
import {
|
|
4617
5327
|
buildApplicationServer,
|
|
4618
5328
|
reserveLoopbackPort,
|
|
4619
5329
|
startApplicationProcess,
|
|
5330
|
+
startLoopbackServer,
|
|
4620
5331
|
stopNodeServer,
|
|
4621
5332
|
waitForApplicationProcess,
|
|
4622
|
-
waitForApplicationResponse,
|
|
4623
|
-
waitForLoopbackResponse,
|
|
4624
|
-
waitForSocketClose,
|
|
4625
5333
|
} from '../../setupServer.js'
|
|
5334
|
+
import { createRecorder, waitForEvent } from '../../setup.js'
|
|
4626
5335
|
|
|
4627
5336
|
beforeAll(() => buildApplicationServer(), 60_000)
|
|
4628
5337
|
|
|
4629
|
-
describe('
|
|
4630
|
-
|
|
4631
|
-
|
|
5338
|
+
describe('application dispatcher', () => {
|
|
5339
|
+
const state = { connection: { encrypted: false } }
|
|
5340
|
+
|
|
5341
|
+
it('returns the typed shared application record for the health route', async () => {
|
|
5342
|
+
const dispatcher = createApplicationDispatcher()
|
|
4632
5343
|
try {
|
|
4633
|
-
|
|
4634
|
-
|
|
5344
|
+
const response = await dispatcher.handle(
|
|
5345
|
+
new Request(\`http://application.test\${APP_HEALTH_PATH}\`),
|
|
5346
|
+
state,
|
|
5347
|
+
)
|
|
4635
5348
|
|
|
4636
|
-
const response = await fetch(server.url)
|
|
4637
5349
|
expect(response.status).toBe(200)
|
|
4638
|
-
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
4639
|
-
expect(response.headers.get('content-type')).toBe('application/json; charset=utf-8')
|
|
4640
5350
|
expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
|
|
5351
|
+
expect(APP_HEALTH_PATH).toBe('/health')
|
|
4641
5352
|
} finally {
|
|
4642
|
-
|
|
4643
|
-
await server.stop()
|
|
4644
|
-
}
|
|
4645
|
-
})
|
|
4646
|
-
|
|
4647
|
-
it('rejects unsupported methods and unknown routes', async () => {
|
|
4648
|
-
const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
|
|
4649
|
-
try {
|
|
4650
|
-
await server.start()
|
|
4651
|
-
const method = await fetch(server.url, { method: 'POST' })
|
|
4652
|
-
expect(method.status).toBe(405)
|
|
4653
|
-
expect(method.headers.get('allow')).toBe(APP_HEALTH_METHOD)
|
|
4654
|
-
expect(method.headers.get('content-type')).toBe('text/plain; charset=utf-8')
|
|
4655
|
-
expect(await method.text()).toBe('Method Not Allowed')
|
|
4656
|
-
const route = await fetch(\`\${server.url}/missing\`)
|
|
4657
|
-
expect(route.status).toBe(404)
|
|
4658
|
-
expect(route.headers.get('content-type')).toBe('text/plain; charset=utf-8')
|
|
4659
|
-
expect(await route.text()).toBe('Not Found')
|
|
4660
|
-
expect(APP_HEALTH_PATH).toBe('/')
|
|
4661
|
-
} finally {
|
|
4662
|
-
await server.stop()
|
|
5353
|
+
dispatcher.destroy()
|
|
4663
5354
|
}
|
|
4664
5355
|
})
|
|
4665
5356
|
|
|
4666
|
-
it('
|
|
4667
|
-
const
|
|
5357
|
+
it('distinguishes a wrong method from a missing route', async () => {
|
|
5358
|
+
const dispatcher = createApplicationDispatcher()
|
|
4668
5359
|
try {
|
|
4669
|
-
await
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
const response = await fetch(server.url)
|
|
4673
|
-
return { status: response.status, body: await response.json() }
|
|
4674
|
-
}),
|
|
4675
|
-
)
|
|
4676
|
-
expect(responses).toHaveLength(8)
|
|
4677
|
-
for (const response of responses) {
|
|
4678
|
-
expect(response).toEqual({
|
|
4679
|
-
status: 200,
|
|
4680
|
-
body: { name: APP_NAME, status: 'ok' },
|
|
4681
|
-
})
|
|
4682
|
-
}
|
|
4683
|
-
} finally {
|
|
4684
|
-
await server.stop()
|
|
4685
|
-
}
|
|
4686
|
-
})
|
|
4687
|
-
|
|
4688
|
-
it('validates direct options before allocating a listener', () => {
|
|
4689
|
-
expect(() => createApplicationServer({ host: ' ' })).toThrow(
|
|
4690
|
-
expect.objectContaining({ code: 'CONFIG' }),
|
|
4691
|
-
)
|
|
4692
|
-
expect(() => createApplicationServer({ port: Number.NaN })).toThrow(
|
|
4693
|
-
expect.objectContaining({ code: 'CONFIG' }),
|
|
4694
|
-
)
|
|
4695
|
-
expect(() => createApplicationServer({ port: -1 })).toThrow(
|
|
4696
|
-
expect.objectContaining({ code: 'CONFIG' }),
|
|
4697
|
-
)
|
|
4698
|
-
expect(() => createApplicationServer({ timeout: 0 })).toThrow(
|
|
4699
|
-
expect.objectContaining({ code: 'CONFIG' }),
|
|
4700
|
-
)
|
|
4701
|
-
for (const value of [null, 42, [], { host: 42 }, { port: [42] }]) {
|
|
4702
|
-
expect(() => Reflect.apply(createApplicationServer, undefined, [value])).toThrow(
|
|
4703
|
-
expect.objectContaining({ code: 'CONFIG' }),
|
|
5360
|
+
const method = await dispatcher.handle(
|
|
5361
|
+
new Request(\`http://application.test\${APP_HEALTH_PATH}\`, { method: 'POST' }),
|
|
5362
|
+
state,
|
|
4704
5363
|
)
|
|
4705
|
-
|
|
4706
|
-
expect(createApplicationServer({ host: '::1' }).url).toBe('http://[::1]:3000')
|
|
4707
|
-
})
|
|
5364
|
+
const missing = await dispatcher.handle(new Request('http://application.test/missing'), state)
|
|
4708
5365
|
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
const previousTimeout = process.env.APP_START_TIMEOUT
|
|
4713
|
-
try {
|
|
4714
|
-
process.env.APP_HOST = ' 127.0.0.1 '
|
|
4715
|
-
process.env.APP_PORT = '0'
|
|
4716
|
-
process.env.APP_START_TIMEOUT = '250'
|
|
4717
|
-
const server = createApplicationServer()
|
|
4718
|
-
expect(server.host).toBe('127.0.0.1')
|
|
4719
|
-
expect(server.port).toBe(0)
|
|
4720
|
-
|
|
4721
|
-
process.env.APP_PORT = '1e3'
|
|
4722
|
-
expect(() => createApplicationServer()).toThrow(expect.objectContaining({ code: 'CONFIG' }))
|
|
4723
|
-
process.env.APP_PORT = '0'
|
|
4724
|
-
process.env.APP_START_TIMEOUT = '0'
|
|
4725
|
-
expect(() => createApplicationServer()).toThrow(expect.objectContaining({ code: 'CONFIG' }))
|
|
5366
|
+
expect(method.status).toBe(405)
|
|
5367
|
+
expect(method.headers.get('allow')).toContain(APP_HEALTH_METHOD)
|
|
5368
|
+
expect(missing.status).toBe(404)
|
|
4726
5369
|
} finally {
|
|
4727
|
-
|
|
4728
|
-
else process.env.APP_HOST = previousHost
|
|
4729
|
-
if (previousPort === undefined) delete process.env.APP_PORT
|
|
4730
|
-
else process.env.APP_PORT = previousPort
|
|
4731
|
-
if (previousTimeout === undefined) delete process.env.APP_START_TIMEOUT
|
|
4732
|
-
else process.env.APP_START_TIMEOUT = previousTimeout
|
|
5370
|
+
dispatcher.destroy()
|
|
4733
5371
|
}
|
|
4734
5372
|
})
|
|
5373
|
+
})
|
|
4735
5374
|
|
|
4736
|
-
|
|
5375
|
+
describe('ApplicationServer', () => {
|
|
5376
|
+
it('composes the real substrate on loopback with security and repeatable lifecycle', async () => {
|
|
4737
5377
|
const server = createApplicationServer({
|
|
4738
|
-
host: '127.0.0.1',
|
|
4739
|
-
port: 0,
|
|
4740
|
-
timeout: 1_000,
|
|
5378
|
+
server: { host: '127.0.0.1', port: 0, timeout: 1_000 },
|
|
4741
5379
|
})
|
|
4742
|
-
const controller = new AbortController()
|
|
4743
|
-
controller.abort(new Error('cancelled by test'))
|
|
4744
5380
|
try {
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
expect(
|
|
4748
|
-
expect(server.
|
|
4749
|
-
|
|
4750
|
-
await server.stop()
|
|
5381
|
+
expect(server.host).toBe('127.0.0.1')
|
|
5382
|
+
expect(server.port).toBeUndefined()
|
|
5383
|
+
expect(server.url).toBeUndefined()
|
|
5384
|
+
expect(server.status).toBe('idle')
|
|
4751
5385
|
await server.start()
|
|
4752
|
-
expect(
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
5386
|
+
expect(server.status).toBe('listening')
|
|
5387
|
+
expect(server.port).toEqual(expect.any(Number))
|
|
5388
|
+
const firstUrl = server.url
|
|
5389
|
+
if (firstUrl === undefined) throw new Error('Expected a bound application URL')
|
|
5390
|
+
|
|
5391
|
+
const first = await fetch(\`\${firstUrl}\${APP_HEALTH_PATH}\`)
|
|
5392
|
+
expect(first.status).toBe(200)
|
|
5393
|
+
expect(first.headers.get('x-content-type-options')).toBe('nosniff')
|
|
5394
|
+
expect(first.headers.get('x-frame-options')).toBe('DENY')
|
|
5395
|
+
expect(first.headers.get('content-security-policy')).not.toBeNull()
|
|
5396
|
+
expect(first.headers.get('x-request-id')).not.toBeNull()
|
|
5397
|
+
expect(await first.json()).toEqual({ name: APP_NAME, status: 'ok' })
|
|
4757
5398
|
|
|
4758
|
-
it('contains a revoked startup signal without listening or leaking transition state', async () => {
|
|
4759
|
-
const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
|
|
4760
|
-
const revocable = Proxy.revocable(new AbortController().signal, {})
|
|
4761
|
-
revocable.revoke()
|
|
4762
|
-
try {
|
|
4763
|
-
await expect(Reflect.apply(server.start, server, [revocable.proxy])).rejects.toMatchObject({
|
|
4764
|
-
code: 'LIFECYCLE',
|
|
4765
|
-
})
|
|
4766
|
-
expect(server.listening).toBe(false)
|
|
4767
|
-
|
|
4768
|
-
await server.start()
|
|
4769
|
-
expect((await fetch(server.url)).status).toBe(200)
|
|
4770
|
-
} finally {
|
|
4771
5399
|
await server.stop()
|
|
4772
|
-
}
|
|
4773
|
-
})
|
|
4774
|
-
|
|
4775
|
-
it('fails closed on a port collision and preserves the owning server', async () => {
|
|
4776
|
-
const owner = createApplicationServer({ host: '127.0.0.1', port: 0 })
|
|
4777
|
-
let blocked: ReturnType<typeof createApplicationServer> | undefined
|
|
4778
|
-
try {
|
|
4779
|
-
await owner.start()
|
|
4780
|
-
blocked = createApplicationServer({ host: '127.0.0.1', port: owner.port })
|
|
4781
|
-
await expect(blocked.start()).rejects.toMatchObject({ code: 'LIFECYCLE' })
|
|
4782
|
-
expect(blocked.listening).toBe(false)
|
|
4783
|
-
expect((await fetch(owner.url)).status).toBe(200)
|
|
4784
|
-
} finally {
|
|
4785
|
-
await blocked?.stop()
|
|
4786
|
-
await owner.stop()
|
|
4787
|
-
}
|
|
4788
|
-
})
|
|
4789
|
-
|
|
4790
|
-
it('restarts cleanly after a completed stop', async () => {
|
|
4791
|
-
const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
|
|
4792
|
-
try {
|
|
4793
|
-
await server.start()
|
|
4794
|
-
await server.stop()
|
|
4795
|
-
await Promise.all([server.start(), server.start()])
|
|
4796
|
-
expect((await fetch(server.url)).status).toBe(200)
|
|
4797
|
-
} finally {
|
|
4798
5400
|
await server.stop()
|
|
4799
|
-
|
|
4800
|
-
|
|
5401
|
+
expect(server.port).toBeUndefined()
|
|
5402
|
+
expect(server.url).toBeUndefined()
|
|
5403
|
+
expect(server.status).toBe('stopped')
|
|
4801
5404
|
|
|
4802
|
-
it('requests a fresh ephemeral port when the previous port is occupied', async () => {
|
|
4803
|
-
const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
|
|
4804
|
-
const occupant = createServer()
|
|
4805
|
-
try {
|
|
4806
5405
|
await server.start()
|
|
4807
|
-
const
|
|
5406
|
+
const secondUrl = server.url
|
|
5407
|
+
if (secondUrl === undefined) throw new Error('Expected a rebound application URL')
|
|
5408
|
+
const second = await fetch(\`\${secondUrl}\${APP_HEALTH_PATH}\`)
|
|
5409
|
+
expect(second.status).toBe(200)
|
|
4808
5410
|
await server.stop()
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
await server.start()
|
|
4813
|
-
|
|
4814
|
-
expect(server.port).not.toBe(previousPort)
|
|
4815
|
-
expect((await fetch(server.url)).status).toBe(200)
|
|
5411
|
+
await server.destroy()
|
|
5412
|
+
await server.destroy()
|
|
5413
|
+
expect(server.status).toBe('stopped')
|
|
4816
5414
|
} finally {
|
|
4817
|
-
await server.
|
|
4818
|
-
await stopNodeServer(occupant)
|
|
5415
|
+
await server.destroy()
|
|
4819
5416
|
}
|
|
4820
5417
|
})
|
|
4821
5418
|
|
|
4822
|
-
it('
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
} finally {
|
|
4833
|
-
await server.stop()
|
|
5419
|
+
it('rejects invalid grouped host and port options before binding', () => {
|
|
5420
|
+
for (const value of [
|
|
5421
|
+
{ server: { host: ' ' } },
|
|
5422
|
+
{ server: { port: Number.NaN } },
|
|
5423
|
+
{ server: { port: -1 } },
|
|
5424
|
+
{ server: { timeout: 0 } },
|
|
5425
|
+
]) {
|
|
5426
|
+
expect(() => Reflect.apply(createApplicationServer, undefined, [value])).toThrow(
|
|
5427
|
+
expect.objectContaining({ code: 'CONFIG' }),
|
|
5428
|
+
)
|
|
4834
5429
|
}
|
|
4835
5430
|
})
|
|
5431
|
+
})
|
|
4836
5432
|
|
|
4837
|
-
|
|
4838
|
-
|
|
5433
|
+
describe('ApplicationServerRunner', () => {
|
|
5434
|
+
it('serves a real request after a restart queued during shutdown', async () => {
|
|
5435
|
+
const port = await reserveLoopbackPort()
|
|
5436
|
+
const code = process.exitCode
|
|
5437
|
+
const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
|
|
5438
|
+
const runner = new ApplicationServerRunner(server)
|
|
4839
5439
|
try {
|
|
4840
|
-
|
|
4841
|
-
const
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
5440
|
+
process.exitCode = undefined
|
|
5441
|
+
const first = waitForEvent(runner.emitter, 'ready')
|
|
5442
|
+
runner.start()
|
|
5443
|
+
await first
|
|
5444
|
+
const stopped = runner.stop()
|
|
5445
|
+
const second = waitForEvent(runner.emitter, 'ready')
|
|
5446
|
+
runner.start()
|
|
5447
|
+
await stopped
|
|
4846
5448
|
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
5449
|
+
const [url] = await second
|
|
5450
|
+
const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
|
|
5451
|
+
expect(response.status).toBe(200)
|
|
5452
|
+
expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
|
|
5453
|
+
const stopping = runner.stop()
|
|
5454
|
+
expect(runner.stop()).toBe(stopping)
|
|
5455
|
+
await stopping
|
|
5456
|
+
expect(process.exitCode).toBeUndefined()
|
|
4853
5457
|
} finally {
|
|
4854
|
-
|
|
5458
|
+
process.exitCode = code
|
|
5459
|
+
await runner.stop()
|
|
4855
5460
|
}
|
|
4856
5461
|
})
|
|
4857
5462
|
|
|
4858
|
-
it('
|
|
4859
|
-
const
|
|
4860
|
-
const
|
|
5463
|
+
it('cancels a startup still in flight so an immediate stop leaves the port free', async () => {
|
|
5464
|
+
const port = await reserveLoopbackPort()
|
|
5465
|
+
const ready = createRecorder<[url: string]>()
|
|
5466
|
+
const failed = createRecorder<[error: unknown]>()
|
|
5467
|
+
const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
|
|
5468
|
+
const runner = new ApplicationServerRunner(server, {
|
|
5469
|
+
on: { ready: ready.handler, fail: failed.handler },
|
|
5470
|
+
})
|
|
4861
5471
|
try {
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
}
|
|
4869
|
-
const overflow = connect({ host: server.host, port: server.port })
|
|
5472
|
+
runner.start()
|
|
5473
|
+
await runner.stop()
|
|
5474
|
+
expect(ready.count).toBe(0)
|
|
5475
|
+
expect(failed.count).toBe(0)
|
|
5476
|
+
|
|
5477
|
+
const released = await startLoopbackServer(port)
|
|
4870
5478
|
try {
|
|
4871
|
-
|
|
4872
|
-
await once(overflow, 'connect')
|
|
4873
|
-
await closed
|
|
4874
|
-
expect(overflow.destroyed).toBe(true)
|
|
5479
|
+
expect(released.listening).toBe(true)
|
|
4875
5480
|
} finally {
|
|
4876
|
-
|
|
5481
|
+
await stopNodeServer(released)
|
|
4877
5482
|
}
|
|
4878
|
-
const released = sockets.pop()
|
|
4879
|
-
if (released === undefined) throw new Error('expected a held connection')
|
|
4880
|
-
const releasedClose = waitForSocketClose(released)
|
|
4881
|
-
released.destroy()
|
|
4882
|
-
await releasedClose
|
|
4883
|
-
const response = await waitForLoopbackResponse(server.port)
|
|
4884
|
-
expect(response.status).toBe(200)
|
|
4885
5483
|
} finally {
|
|
4886
|
-
|
|
4887
|
-
await server.stop()
|
|
5484
|
+
await runner.stop()
|
|
4888
5485
|
}
|
|
4889
5486
|
})
|
|
4890
5487
|
|
|
4891
|
-
it('
|
|
5488
|
+
it('announces readiness exactly once before the first response and releases on SIGTERM', async () => {
|
|
4892
5489
|
const port = await reserveLoopbackPort()
|
|
4893
5490
|
const application = startApplicationProcess(port)
|
|
4894
5491
|
try {
|
|
4895
|
-
|
|
5492
|
+
await application.ready
|
|
5493
|
+
const announcement = \`[READY] \${APP_NAME} http://127.0.0.1:\${port}\\n\`
|
|
5494
|
+
expect(application.output()).toBe(announcement)
|
|
5495
|
+
|
|
5496
|
+
const response = await fetch(\`http://127.0.0.1:\${port}\${APP_HEALTH_PATH}\`)
|
|
4896
5497
|
expect(response.status).toBe(200)
|
|
4897
5498
|
expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
|
|
5499
|
+
expect(application.output().split('[READY]')).toHaveLength(2)
|
|
4898
5500
|
|
|
4899
5501
|
expect(application.child.kill('SIGTERM')).toBe(true)
|
|
4900
5502
|
const exited = await waitForApplicationProcess(application)
|
|
@@ -4904,10 +5506,8 @@ describe('ApplicationServer', () => {
|
|
|
4904
5506
|
: { code: 0, signal: null },
|
|
4905
5507
|
)
|
|
4906
5508
|
|
|
4907
|
-
const released =
|
|
5509
|
+
const released = await startLoopbackServer(port)
|
|
4908
5510
|
try {
|
|
4909
|
-
released.listen(port, '127.0.0.1')
|
|
4910
|
-
await once(released, 'listening')
|
|
4911
5511
|
expect(released.listening).toBe(true)
|
|
4912
5512
|
} finally {
|
|
4913
5513
|
await stopNodeServer(released)
|
|
@@ -4920,26 +5520,64 @@ describe('ApplicationServer', () => {
|
|
|
4920
5520
|
}
|
|
4921
5521
|
})
|
|
4922
5522
|
|
|
4923
|
-
it('
|
|
5523
|
+
it('never announces readiness when a real bind fails', async () => {
|
|
4924
5524
|
const port = await reserveLoopbackPort()
|
|
4925
|
-
const owner =
|
|
4926
|
-
owner.listen(port, '127.0.0.1')
|
|
4927
|
-
await once(owner, 'listening')
|
|
5525
|
+
const owner = await startLoopbackServer(port)
|
|
4928
5526
|
const application = startApplicationProcess(port)
|
|
4929
5527
|
try {
|
|
4930
5528
|
const exited = await waitForApplicationProcess(application)
|
|
4931
5529
|
expect(exited).toEqual({ code: 1, signal: null })
|
|
4932
5530
|
expect(application.output()).toBe('[LIFECYCLE] Application server lifecycle failed\\n')
|
|
5531
|
+
expect(application.output()).not.toContain('[READY]')
|
|
4933
5532
|
expect(application.output()).not.toContain('EADDRINUSE')
|
|
4934
|
-
expect(application.output()).not.toContain('context')
|
|
4935
|
-
expect(application.output()).not.toContain('cause')
|
|
4936
|
-
expect(application.output()).not.toContain('at ')
|
|
4937
5533
|
expect(owner.listening).toBe(true)
|
|
4938
5534
|
} finally {
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
5535
|
+
await stopNodeServer(owner)
|
|
5536
|
+
}
|
|
5537
|
+
})
|
|
5538
|
+
|
|
5539
|
+
it('isolates a throwing fail listener, releases ownership, and starts again', async () => {
|
|
5540
|
+
const port = await reserveLoopbackPort()
|
|
5541
|
+
const owner = await startLoopbackServer(port)
|
|
5542
|
+
const interrupts = process.listenerCount('SIGINT')
|
|
5543
|
+
const terminations = process.listenerCount('SIGTERM')
|
|
5544
|
+
// The reporter owns the process exit code, so this in-process failure records the
|
|
5545
|
+
// code it set and then restores whatever the surrounding run had.
|
|
5546
|
+
const code = process.exitCode
|
|
5547
|
+
const errors = createRecorder<[error: unknown, event: string]>()
|
|
5548
|
+
const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
|
|
5549
|
+
const runner = new ApplicationServerRunner(server, {
|
|
5550
|
+
on: {
|
|
5551
|
+
fail: () => {
|
|
5552
|
+
throw new Error('listener boom')
|
|
5553
|
+
},
|
|
5554
|
+
},
|
|
5555
|
+
error: errors.handler,
|
|
5556
|
+
})
|
|
5557
|
+
try {
|
|
5558
|
+
process.exitCode = undefined
|
|
5559
|
+
const failed = waitForEvent(runner.emitter, 'fail')
|
|
5560
|
+
runner.start()
|
|
5561
|
+
expect(process.listenerCount('SIGINT')).toBe(interrupts + 1)
|
|
5562
|
+
expect(process.listenerCount('SIGTERM')).toBe(terminations + 1)
|
|
5563
|
+
|
|
5564
|
+
const [failure] = await failed
|
|
5565
|
+
expect(failure).toEqual(expect.objectContaining({ code: 'LIFECYCLE' }))
|
|
5566
|
+
expect(errors.count).toBe(1)
|
|
5567
|
+
expect(errors.calls[0]?.[1]).toBe('fail')
|
|
5568
|
+
expect(process.listenerCount('SIGINT')).toBe(interrupts)
|
|
5569
|
+
expect(process.listenerCount('SIGTERM')).toBe(terminations)
|
|
5570
|
+
expect(process.exitCode).toBe(1)
|
|
5571
|
+
|
|
5572
|
+
await stopNodeServer(owner)
|
|
5573
|
+
const ready = waitForEvent(runner.emitter, 'ready')
|
|
5574
|
+
runner.start()
|
|
5575
|
+
const [url] = await ready
|
|
5576
|
+
const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
|
|
5577
|
+
expect(response.status).toBe(200)
|
|
5578
|
+
} finally {
|
|
5579
|
+
process.exitCode = code
|
|
5580
|
+
await runner.stop()
|
|
4943
5581
|
await stopNodeServer(owner)
|
|
4944
5582
|
}
|
|
4945
5583
|
})
|
|
@@ -4951,74 +5589,12 @@ describe('ApplicationServer', () => {
|
|
|
4951
5589
|
expect(exited).toEqual({ code: 1, signal: null })
|
|
4952
5590
|
expect(application.output()).toBe('[CONFIG] Application server configuration failed\\n')
|
|
4953
5591
|
expect(application.output()).not.toContain(secret)
|
|
5592
|
+
expect(application.output()).not.toContain('[READY]')
|
|
4954
5593
|
expect(application.output()).not.toContain('context')
|
|
4955
5594
|
expect(application.output()).not.toContain('cause')
|
|
4956
5595
|
expect(application.output()).not.toContain('at ')
|
|
4957
5596
|
})
|
|
4958
|
-
|
|
4959
|
-
it('releases process listeners through explicit, repeated, convenience, and signal cleanup', async () => {
|
|
4960
|
-
const signalCount = process.listenerCount('SIGTERM')
|
|
4961
|
-
const interruptCount = process.listenerCount('SIGINT')
|
|
4962
|
-
const runner = new ApplicationServerRunner({ host: '127.0.0.1', port: 0 })
|
|
4963
|
-
try {
|
|
4964
|
-
expect(runner.start()).toBeUndefined()
|
|
4965
|
-
expect(runner.start()).toBeUndefined()
|
|
4966
|
-
expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
|
|
4967
|
-
expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
|
|
4968
|
-
|
|
4969
|
-
await runner.stop()
|
|
4970
|
-
await runner.stop()
|
|
4971
|
-
expect(process.listenerCount('SIGTERM')).toBe(signalCount)
|
|
4972
|
-
expect(process.listenerCount('SIGINT')).toBe(interruptCount)
|
|
4973
|
-
|
|
4974
|
-
const convenience = startApplicationServer({ host: '127.0.0.1', port: 0 })
|
|
4975
|
-
expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
|
|
4976
|
-
expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
|
|
4977
|
-
await convenience.stop()
|
|
4978
|
-
expect(process.listenerCount('SIGTERM')).toBe(signalCount)
|
|
4979
|
-
expect(process.listenerCount('SIGINT')).toBe(interruptCount)
|
|
4980
|
-
|
|
4981
|
-
runner.start()
|
|
4982
|
-
expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
|
|
4983
|
-
expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
|
|
4984
|
-
process.emit('SIGTERM')
|
|
4985
|
-
await runner.stop()
|
|
4986
|
-
expect(process.listenerCount('SIGTERM')).toBe(signalCount)
|
|
4987
|
-
expect(process.listenerCount('SIGINT')).toBe(interruptCount)
|
|
4988
|
-
} finally {
|
|
4989
|
-
await runner.stop()
|
|
4990
|
-
}
|
|
4991
|
-
})
|
|
4992
|
-
|
|
4993
|
-
it('keeps a newer runner generation owned when an older start fails during restart', async () => {
|
|
4994
|
-
const signalCount = process.listenerCount('SIGTERM')
|
|
4995
|
-
const interruptCount = process.listenerCount('SIGINT')
|
|
4996
|
-
const previousExitCode = process.exitCode
|
|
4997
|
-
const port = await reserveLoopbackPort()
|
|
4998
|
-
const owner = createServer()
|
|
4999
|
-
owner.listen(port, '127.0.0.1')
|
|
5000
|
-
await once(owner, 'listening')
|
|
5001
|
-
const runner = new ApplicationServerRunner({ host: '127.0.0.1', port })
|
|
5002
|
-
try {
|
|
5003
|
-
runner.start()
|
|
5004
|
-
await Promise.resolve()
|
|
5005
|
-
const stopping = runner.stop()
|
|
5006
|
-
runner.start()
|
|
5007
|
-
await stopNodeServer(owner)
|
|
5008
|
-
await stopping
|
|
5009
|
-
|
|
5010
|
-
const response = await waitForLoopbackResponse(port)
|
|
5011
|
-
expect(response.status).toBe(200)
|
|
5012
|
-
expect(process.exitCode).toBe(previousExitCode)
|
|
5013
|
-
expect(process.listenerCount('SIGTERM')).toBe(signalCount + 1)
|
|
5014
|
-
expect(process.listenerCount('SIGINT')).toBe(interruptCount + 1)
|
|
5015
|
-
} finally {
|
|
5016
|
-
await runner.stop()
|
|
5017
|
-
await stopNodeServer(owner)
|
|
5018
|
-
process.exitCode = previousExitCode
|
|
5019
|
-
}
|
|
5020
|
-
})
|
|
5021
|
-
})
|
|
5597
|
+
}){{boundary}}
|
|
5022
5598
|
`
|
|
5023
5599
|
}),
|
|
5024
5600
|
appServerParsersTest: Object.freeze({
|
|
@@ -5042,6 +5618,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
5042
5618
|
|
|
5043
5619
|
describe('application environment parsers', () => {
|
|
5044
5620
|
it('accepts port boundaries and trims a host', () => {
|
|
5621
|
+
expect(parseApplicationPort(3000)).toBe(3000)
|
|
5045
5622
|
expect(parseApplicationPort('0')).toBe(0)
|
|
5046
5623
|
expect(parseApplicationPort('65535')).toBe(65_535)
|
|
5047
5624
|
expect(parseApplicationPort(' 3000 ')).toBe(3000)
|
|
@@ -5053,6 +5630,19 @@ describe('application environment parsers', () => {
|
|
|
5053
5630
|
expect(parseApplicationStartTimeout(String(MAX_APP_START_TIMEOUT))).toBe(MAX_APP_START_TIMEOUT)
|
|
5054
5631
|
})
|
|
5055
5632
|
|
|
5633
|
+
it('never coerces hostile numeric objects through toString', () => {
|
|
5634
|
+
let calls = 0
|
|
5635
|
+
const hostile = {
|
|
5636
|
+
toString() {
|
|
5637
|
+
calls += 1
|
|
5638
|
+
return '3000'
|
|
5639
|
+
},
|
|
5640
|
+
}
|
|
5641
|
+
|
|
5642
|
+
expect(() => parseApplicationPort(hostile)).toThrow(ApplicationServerError)
|
|
5643
|
+
expect(calls).toBe(0)
|
|
5644
|
+
})
|
|
5645
|
+
|
|
5056
5646
|
it.each(['', '-1', '65536', '1.5', '+1', '0x10', '1e3', 'NaN', 'Infinity'])(
|
|
5057
5647
|
'rejects hostile APP_PORT value %s',
|
|
5058
5648
|
(value) => {
|
|
@@ -5094,7 +5684,15 @@ describe('application environment parsers', () => {
|
|
|
5094
5684
|
expect(() => parseApplicationHost(value)).toThrow(expect.objectContaining({ code: 'CONFIG' }))
|
|
5095
5685
|
})
|
|
5096
5686
|
|
|
5097
|
-
it
|
|
5687
|
+
it('parses the exact grouped server record including its timeout leaf', () => {
|
|
5688
|
+
expect(
|
|
5689
|
+
parseApplicationServerOptions({
|
|
5690
|
+
server: { host: ' 127.0.0.1 ', port: 0, timeout: 250 },
|
|
5691
|
+
}),
|
|
5692
|
+
).toEqual({ server: { host: '127.0.0.1', port: 0, timeout: 250 } })
|
|
5693
|
+
})
|
|
5694
|
+
|
|
5695
|
+
it.each([null, 42, [], { server: null }, { server: { port: [42] } }, { server: { timeout: 0 } }])(
|
|
5098
5696
|
'rejects hostile option container or leaf value %#',
|
|
5099
5697
|
(value) => {
|
|
5100
5698
|
expect(() => parseApplicationServerOptions(value)).toThrow(
|
|
@@ -5104,11 +5702,14 @@ describe('application environment parsers', () => {
|
|
|
5104
5702
|
)
|
|
5105
5703
|
|
|
5106
5704
|
it.each([
|
|
5107
|
-
Object.create({ host: '0.0.0.0' }),
|
|
5705
|
+
Object.create({ server: { host: '0.0.0.0' } }),
|
|
5108
5706
|
{ post: 0 },
|
|
5109
5707
|
new Date(),
|
|
5110
|
-
{
|
|
5111
|
-
|
|
5708
|
+
{ server: Object.create({ host: '0.0.0.0' }) },
|
|
5709
|
+
{ server: { post: 0 } },
|
|
5710
|
+
{ server: { [Symbol('host')]: '0.0.0.0' } },
|
|
5711
|
+
Object.defineProperty({}, 'server', { get: () => ({ host: '0.0.0.0' }) }),
|
|
5712
|
+
{ server: Object.defineProperty({}, 'host', { get: () => '0.0.0.0' }) },
|
|
5112
5713
|
new Proxy(
|
|
5113
5714
|
{},
|
|
5114
5715
|
{
|
|
@@ -5117,6 +5718,16 @@ describe('application environment parsers', () => {
|
|
|
5117
5718
|
},
|
|
5118
5719
|
},
|
|
5119
5720
|
),
|
|
5721
|
+
{
|
|
5722
|
+
server: new Proxy(
|
|
5723
|
+
{},
|
|
5724
|
+
{
|
|
5725
|
+
ownKeys: () => {
|
|
5726
|
+
throw new Error('hostile nested ownKeys trap')
|
|
5727
|
+
},
|
|
5728
|
+
},
|
|
5729
|
+
),
|
|
5730
|
+
},
|
|
5120
5731
|
])(
|
|
5121
5732
|
'rejects inherited, unknown, symbolic, accessor, instance, and hostile proxy options %#',
|
|
5122
5733
|
(value) => {
|
|
@@ -5126,6 +5737,19 @@ describe('application environment parsers', () => {
|
|
|
5126
5737
|
},
|
|
5127
5738
|
)
|
|
5128
5739
|
|
|
5740
|
+
it('does not invoke grouped option accessors', () => {
|
|
5741
|
+
let reads = 0
|
|
5742
|
+
const server = Object.defineProperty({}, 'timeout', {
|
|
5743
|
+
get() {
|
|
5744
|
+
reads += 1
|
|
5745
|
+
return 250
|
|
5746
|
+
},
|
|
5747
|
+
})
|
|
5748
|
+
|
|
5749
|
+
expect(() => parseApplicationServerOptions({ server })).toThrow(ApplicationServerError)
|
|
5750
|
+
expect(reads).toBe(0)
|
|
5751
|
+
})
|
|
5752
|
+
|
|
5129
5753
|
it('never reflects a hostile symbolic option name into diagnostics', () => {
|
|
5130
5754
|
let caught: unknown
|
|
5131
5755
|
try {
|
|
@@ -5175,9 +5799,7 @@ ${IMPORT_KEYWORD} { createSource, parseManifest } from '@orkestrel/guide'
|
|
|
5175
5799
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_ROOT = fileURLToPath(new URL('../', import.meta.url))
|
|
5176
5800
|
|
|
5177
5801
|
/** Repository roots whose TypeScript and Markdown files participate in guide parity. */
|
|
5178
|
-
${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze(
|
|
5179
|
-
{{walkDirs}}
|
|
5180
|
-
])
|
|
5802
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze({{walkDirs}})
|
|
5181
5803
|
|
|
5182
5804
|
{{specifiers}}
|
|
5183
5805
|
|
|
@@ -5363,14 +5985,30 @@ for (const entry of GUIDE_MANIFEST) {
|
|
|
5363
5985
|
* @example
|
|
5364
5986
|
* ```ts
|
|
5365
5987
|
* hostGroup('AGENTS.md') // 'docs'
|
|
5366
|
-
* hostGroup('.agents') // 'orchestration'
|
|
5367
|
-
* hostGroup('.claude') // 'orchestration'
|
|
5368
|
-
* hostGroup('.
|
|
5988
|
+
* hostGroup('.agents/orchestration.md') // 'orchestration'
|
|
5989
|
+
* hostGroup('.claude/rules') // 'orchestration'
|
|
5990
|
+
* hostGroup('.cursor/rules') // 'orchestration'
|
|
5991
|
+
* hostGroup('.mcp.json') // 'orchestration'
|
|
5992
|
+
* hostGroup('.oxlintrc.json') // 'configs'
|
|
5369
5993
|
* ```
|
|
5994
|
+
*
|
|
5995
|
+
* @remarks
|
|
5996
|
+
* Takes a `HOST_PATHS` entry, so every example above is one. A bare directory
|
|
5997
|
+
* name is not: `ORCHESTRATION_PATH_PREFIXES` entries carry a trailing slash, so
|
|
5998
|
+
* `hostGroup('.cursor')` is `configs`, and no vendored entry has that form.
|
|
5999
|
+
*
|
|
6000
|
+
* Below the `docs` branch the split is by what a path governs rather than where
|
|
6001
|
+
* it sits, which is why both MCP registrations — `.mcp.json` and
|
|
6002
|
+
* `.cursor/mcp.json` — group with the harness bridges instead of with the root
|
|
6003
|
+
* dotfiles beside them. The `docs` branch is checked first and is deliberately
|
|
6004
|
+
* positional: `AGENTS.md`, `CLAUDE.md`, and `LICENSE` are the root documents, and
|
|
6005
|
+
* `CLAUDE.md` stays there as a root document even though it is also a harness
|
|
6006
|
+
* bridge. A plan selecting `orchestration` therefore carries two of the three
|
|
6007
|
+
* bridges; a plan selecting `docs` carries the third.
|
|
5370
6008
|
*/
|
|
5371
6009
|
function hostGroup(path) {
|
|
5372
6010
|
if (path === "AGENTS.md" || path === "CLAUDE.md" || path === "LICENSE") return "docs";
|
|
5373
|
-
if (
|
|
6011
|
+
if (matchesOrchestrationPath(path)) return "orchestration";
|
|
5374
6012
|
if (path.startsWith("tests/")) return "tests";
|
|
5375
6013
|
if (path === "guides/src/guide.md" || path === "guides/src/scaffold.md") return "guides";
|
|
5376
6014
|
return "configs";
|
|
@@ -5569,7 +6207,9 @@ function devDependenciesFor(spec) {
|
|
|
5569
6207
|
return {
|
|
5570
6208
|
...dependencies,
|
|
5571
6209
|
...spec.src.includes("browser") ? SOURCE_BROWSER_DEV_DEPENDENCIES : {},
|
|
6210
|
+
...spec.app.length > 0 ? APP_DEV_DEPENDENCIES : {},
|
|
5572
6211
|
...spec.app.includes("browser") ? APP_BROWSER_DEV_DEPENDENCIES : {},
|
|
6212
|
+
...spec.app.includes("server") ? APP_SERVER_DEV_DEPENDENCIES : {},
|
|
5573
6213
|
...spec.showcase ? { "vite-plugin-singlefile": "^2.3.3" } : {},
|
|
5574
6214
|
...spec.bin ? { "@vitest/browser-playwright": SOURCE_BROWSER_DEV_DEPENDENCIES["@vitest/browser-playwright"] } : {}
|
|
5575
6215
|
};
|
|
@@ -5624,6 +6264,7 @@ function packageManifest(spec) {
|
|
|
5624
6264
|
...hasSource || spec.bin ? ["npm run test:src"] : [],
|
|
5625
6265
|
...spec.app.length > 0 ? ["npm run test:app"] : [],
|
|
5626
6266
|
"npm run test:policy",
|
|
6267
|
+
"npm run test:config",
|
|
5627
6268
|
"npm run test:guides"
|
|
5628
6269
|
].join(" && ");
|
|
5629
6270
|
if (hasSource || spec.bin) {
|
|
@@ -5633,12 +6274,16 @@ function packageManifest(spec) {
|
|
|
5633
6274
|
if (spec.bin) scripts["test:src:bin"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:bin";
|
|
5634
6275
|
if (spec.integration) scripts["test:integration"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project integration";
|
|
5635
6276
|
if (spec.bin && spec.integration) scripts["test:equivalence"] = "node -e \"const c=require('node:child_process'),p=process.env.npm_execpath;if(p===undefined)process.exit(1);const r=c.spawnSync(process.execPath,[p,'run','test:integration'],{stdio:'inherit',env:{...process.env,SCAFFOLD_BOUNDARY_EQUIVALENCE:'1'}});process.exit(r.status??1)\"";
|
|
5636
|
-
if (spec.
|
|
6277
|
+
if (spec.services.length > 0) {
|
|
6278
|
+
scripts["test:service"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.services.map((service) => `--project service:${service}`).join(" ");
|
|
6279
|
+
for (const service of spec.services) scripts[`test:service:${service}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project service:${service}`;
|
|
6280
|
+
}
|
|
5637
6281
|
if (spec.app.length > 0) {
|
|
5638
6282
|
scripts["test:app"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.app.map((environment) => `--project ${APP_MATRIX[environment].project}`).join(" ");
|
|
5639
6283
|
for (const environment of spec.app) scripts[`test:app:${environment}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project ${APP_MATRIX[environment].project}`;
|
|
5640
6284
|
}
|
|
5641
6285
|
scripts["test:policy"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy";
|
|
6286
|
+
scripts["test:config"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project config";
|
|
5642
6287
|
scripts["test:guides"] = "vitest run --config vite.config.ts --reporter=dot --project guides";
|
|
5643
6288
|
scripts.build = [
|
|
5644
6289
|
"npm run clean",
|
|
@@ -5659,7 +6304,7 @@ function packageManifest(spec) {
|
|
|
5659
6304
|
if (spec.showcase) {
|
|
5660
6305
|
scripts.showcase = `vite --config ${SHOWCASE_CONFIG_PATH}`;
|
|
5661
6306
|
scripts["build:showcase"] = `vite build --config ${SHOWCASE_CONFIG_PATH}`;
|
|
5662
|
-
scripts.show = "npm run build:showcase && npm run copy dist/showcase/index.html demo/showcase.html";
|
|
6307
|
+
scripts.show = "npm run format && npm run build:showcase && npm run copy dist/showcase/index.html demo/showcase.html";
|
|
5663
6308
|
}
|
|
5664
6309
|
}
|
|
5665
6310
|
if (spec.app.includes("server")) {
|
|
@@ -5671,7 +6316,7 @@ function packageManifest(spec) {
|
|
|
5671
6316
|
scripts["build:src:bin"] = "vite build --config configs/src/vite.bin.config.ts";
|
|
5672
6317
|
scripts["build:host"] = "node -e \"import('./dist/src/server/index.js').then((m)=>{const n=m.stageHost(process.cwd(),'dist/host').length;console.log('build-host: staged '+n+' file(s) into dist/host')})\"";
|
|
5673
6318
|
}
|
|
5674
|
-
scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "");
|
|
6319
|
+
scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "") + (spec.services.length > 0 ? " && npm run test:service" : "");
|
|
5675
6320
|
const devDependencies = devDependenciesFor(spec);
|
|
5676
6321
|
const manifest = {
|
|
5677
6322
|
name: hasSource ? `@orkestrel/${spec.name}` : spec.name,
|
|
@@ -5679,13 +6324,15 @@ function packageManifest(spec) {
|
|
|
5679
6324
|
...hasSource ? {} : { private: true },
|
|
5680
6325
|
description: spec.description ?? (hasSource ? `The @orkestrel/${spec.name} package.` : `The ${spec.name} application.`),
|
|
5681
6326
|
keywords: [...spec.keywords].sort(),
|
|
5682
|
-
|
|
5683
|
-
|
|
6327
|
+
...hasSource ? {
|
|
6328
|
+
homepage: `https://github.com/orkestrel/${spec.name}#readme`,
|
|
6329
|
+
bugs: `https://github.com/orkestrel/${spec.name}/issues`
|
|
6330
|
+
} : {},
|
|
5684
6331
|
license: "MIT",
|
|
5685
|
-
repository: {
|
|
6332
|
+
...hasSource ? { repository: {
|
|
5686
6333
|
type: "git",
|
|
5687
6334
|
url: `git+https://github.com/orkestrel/${spec.name}.git`
|
|
5688
|
-
},
|
|
6335
|
+
} } : {},
|
|
5689
6336
|
...spec.bin ? { bin: { scaffold: "./dist/bin/scaffold.js" } } : {},
|
|
5690
6337
|
files: spec.bin ? [
|
|
5691
6338
|
"dist/src",
|
|
@@ -5809,7 +6456,7 @@ function viteMachinery(src, app = [], bin = false, showcase = false) {
|
|
|
5809
6456
|
* @example
|
|
5810
6457
|
* ```ts
|
|
5811
6458
|
* viteProjectRegistrations(['core'], [], { integration: true })
|
|
5812
|
-
* // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'guides' }, { project: 'integration' }]
|
|
6459
|
+
* // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'config' }, { project: 'guides' }, { project: 'integration' }]
|
|
5813
6460
|
* ```
|
|
5814
6461
|
*/
|
|
5815
6462
|
function viteProjectRegistrations(src, app = [], facts = {}) {
|
|
@@ -5832,17 +6479,17 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
|
|
|
5832
6479
|
});
|
|
5833
6480
|
if (environment === "server") registrations.push({ project: "appServer" });
|
|
5834
6481
|
}
|
|
5835
|
-
registrations.push({ project: "policy" }, { project: "guides" });
|
|
6482
|
+
registrations.push({ project: "policy" }, { project: "config" }, { project: "guides" });
|
|
5836
6483
|
if (facts.bin === true) registrations.push({ project: "srcBin" });
|
|
5837
6484
|
if (facts.integration === true) registrations.push({ project: "integration" });
|
|
5838
|
-
|
|
6485
|
+
for (const service of facts.services ?? []) registrations.push({ project: `service${pascalCase(service)}` });
|
|
5839
6486
|
return registrations;
|
|
5840
6487
|
}
|
|
5841
6488
|
/**
|
|
5842
6489
|
* Render the one ordered proof and structural-axis project definition block.
|
|
5843
6490
|
*
|
|
5844
6491
|
* @param facts - Optional structural facts.
|
|
5845
|
-
* @returns Policy, guides, then selected axis project definitions, separated by one blank line.
|
|
6492
|
+
* @returns Policy, config, guides, then selected axis project definitions, separated by one blank line.
|
|
5846
6493
|
*
|
|
5847
6494
|
* @example
|
|
5848
6495
|
* ```ts
|
|
@@ -5850,10 +6497,14 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
|
|
|
5850
6497
|
* ```
|
|
5851
6498
|
*/
|
|
5852
6499
|
function viteProjectDefinitions(facts = {}) {
|
|
5853
|
-
const definitions = [
|
|
6500
|
+
const definitions = [
|
|
6501
|
+
policyViteProject(),
|
|
6502
|
+
configViteProject(),
|
|
6503
|
+
guidesViteProject()
|
|
6504
|
+
];
|
|
5854
6505
|
if (facts.bin === true) definitions.push(binViteProject());
|
|
5855
6506
|
if (facts.integration === true) definitions.push(integrationViteProject(facts));
|
|
5856
|
-
|
|
6507
|
+
for (const service of facts.services ?? []) definitions.push(serviceViteProject(service));
|
|
5857
6508
|
return definitions.join("\n");
|
|
5858
6509
|
}
|
|
5859
6510
|
/**
|
|
@@ -5916,9 +6567,11 @@ import { chromium } from 'playwright'
|
|
|
5916
6567
|
` : "";
|
|
5917
6568
|
const vueImports = needsVue ? `import vue from '@vitejs/plugin-vue'
|
|
5918
6569
|
import { parse as parseVue } from 'vue/compiler-sfc'
|
|
6570
|
+
import { parseStartTag } from '@orkestrel/html'
|
|
5919
6571
|
` : "";
|
|
5920
6572
|
const showcaseImports = needsShowcase ? `import { viteSingleFile } from 'vite-plugin-singlefile'
|
|
5921
6573
|
` : "";
|
|
6574
|
+
const showcaseHashImport = needsShowcase ? "import { createHash } from 'node:crypto'\n" : "";
|
|
5922
6575
|
const viteTypeImports = needsVue ? `import type {
|
|
5923
6576
|
CSSOptions,
|
|
5924
6577
|
HtmlAssetSource,
|
|
@@ -6880,12 +7533,40 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_POLICY =
|
|
|
6880
7533
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_META =
|
|
6881
7534
|
'<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
|
|
6882
7535
|
HTML_SECURITY_POLICY +
|
|
7536
|
+
'"\\n\\t\\t/>'${needsShowcase ? `
|
|
7537
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_POLICY =
|
|
7538
|
+
"default-src 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src data:; font-src data:; script-src-attr 'none'"
|
|
7539
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_META =
|
|
7540
|
+
'<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
|
|
7541
|
+
SHOWCASE_SECURITY_POLICY +
|
|
7542
|
+
'"\\n\\t\\t/>'
|
|
7543
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_BUILD_SECURITY_POLICY =
|
|
7544
|
+
"default-src 'none'; base-uri 'none'; object-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; script-src-attr 'none'"
|
|
7545
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_BUILD_SECURITY_META =
|
|
7546
|
+
'<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
|
|
7547
|
+
SHOWCASE_BUILD_SECURITY_POLICY +
|
|
6883
7548
|
'"\\n\\t\\t/>'
|
|
6884
|
-
|
|
6885
|
-
'<!doctype html>\\n<html lang="en">\\n\\t<head>\\n\\t\\t' + HTML_SECURITY_META + '\\n'
|
|
7549
|
+
` : ""}
|
|
6886
7550
|
|
|
6887
|
-
${EXPORT_KEYWORD} function
|
|
6888
|
-
|
|
7551
|
+
${EXPORT_KEYWORD} function hasSecurityPrologue(html: string, security: string): boolean {
|
|
7552
|
+
const normalized = html.replaceAll('\\r\\n', '\\n')
|
|
7553
|
+
const doctype = '<!doctype html>\\n'
|
|
7554
|
+
if (!normalized.startsWith(doctype)) return false
|
|
7555
|
+
const root = parseStartTag(normalized, doctype.length)
|
|
7556
|
+
return (
|
|
7557
|
+
root !== undefined &&
|
|
7558
|
+
root.name === 'html' &&
|
|
7559
|
+
!root.slashed &&
|
|
7560
|
+
normalized.startsWith('\\n\\t<head>\\n\\t\\t' + security + '\\n', root.next)
|
|
7561
|
+
)
|
|
7562
|
+
}
|
|
7563
|
+
|
|
7564
|
+
${EXPORT_KEYWORD} function maskIgnoredHtml(
|
|
7565
|
+
environmentKeys: ReadonlySet<string>,
|
|
7566
|
+
html: string,
|
|
7567
|
+
security: string,
|
|
7568
|
+
): string {
|
|
7569
|
+
if (!hasSecurityPrologue(html, security)) {
|
|
6889
7570
|
throw new Error(
|
|
6890
7571
|
'[orkestrel-environment-boundary] Browser HTML must preserve the generated security prologue',
|
|
6891
7572
|
)
|
|
@@ -6919,12 +7600,27 @@ ${EXPORT_KEYWORD} function maskIgnoredHtml(environmentKeys: ReadonlySet<string>,
|
|
|
6919
7600
|
)
|
|
6920
7601
|
}
|
|
6921
7602
|
|
|
6922
|
-
${EXPORT_KEYWORD} function
|
|
7603
|
+
${needsShowcase ? `${EXPORT_KEYWORD} function isShowcaseHtmlEntry(filename: string): boolean {
|
|
6923
7604
|
return (
|
|
6924
|
-
physicalPath(filename) ===
|
|
7605
|
+
physicalPath(filename) ===
|
|
7606
|
+
physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/showcase.html'))
|
|
6925
7607
|
)
|
|
6926
7608
|
}
|
|
6927
7609
|
|
|
7610
|
+
` : ""}${EXPORT_KEYWORD} function isBrowserHtmlEntry(filename: string): boolean {
|
|
7611
|
+
${needsShowcase ? `return (
|
|
7612
|
+
isShowcaseHtmlEntry(filename) ||
|
|
7613
|
+
physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
|
|
7614
|
+
)` : `return (
|
|
7615
|
+
physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
|
|
7616
|
+
)`}
|
|
7617
|
+
}${needsShowcase ? `
|
|
7618
|
+
${EXPORT_KEYWORD} function browserHtmlSecurityMeta(filename: string, built: boolean): string | undefined {
|
|
7619
|
+
if (!isBrowserHtmlEntry(filename)) return undefined
|
|
7620
|
+
if (!isShowcaseHtmlEntry(filename)) return HTML_SECURITY_META
|
|
7621
|
+
return built ? SHOWCASE_BUILD_SECURITY_META : SHOWCASE_SECURITY_META
|
|
7622
|
+
}` : ""}
|
|
7623
|
+
|
|
6928
7624
|
${EXPORT_KEYWORD} function prepareHtml(): Plugin {
|
|
6929
7625
|
const environmentKeys = new Set<string>()
|
|
6930
7626
|
return {
|
|
@@ -6941,8 +7637,10 @@ ${EXPORT_KEYWORD} function prepareHtml(): Plugin {
|
|
|
6941
7637
|
transformIndexHtml: {
|
|
6942
7638
|
order: 'pre',
|
|
6943
7639
|
handler(html, context) {
|
|
6944
|
-
|
|
6945
|
-
|
|
7640
|
+
${needsShowcase ? `const security = browserHtmlSecurityMeta(context.filename, false)
|
|
7641
|
+
if (security === undefined) return undefined
|
|
7642
|
+
return maskIgnoredHtml(environmentKeys, html, security)` : `if (!isBrowserHtmlEntry(context.filename)) return undefined
|
|
7643
|
+
return maskIgnoredHtml(environmentKeys, html, HTML_SECURITY_META)`}
|
|
6946
7644
|
},
|
|
6947
7645
|
},
|
|
6948
7646
|
}
|
|
@@ -6966,8 +7664,13 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
|
|
|
6966
7664
|
transformIndexHtml: {
|
|
6967
7665
|
order: 'post',
|
|
6968
7666
|
handler(html, context) {
|
|
6969
|
-
|
|
6970
|
-
|
|
7667
|
+
${needsShowcase ? `const security = browserHtmlSecurityMeta(
|
|
7668
|
+
context.filename,
|
|
7669
|
+
context.bundle !== undefined,
|
|
7670
|
+
)
|
|
7671
|
+
if (security === undefined) return undefined
|
|
7672
|
+
if (!html.includes(security)) {` : `if (!isBrowserHtmlEntry(context.filename)) return undefined
|
|
7673
|
+
if (!html.includes(HTML_SECURITY_META)) {`}
|
|
6971
7674
|
throw new Error(
|
|
6972
7675
|
'[orkestrel-environment-boundary] Browser HTML must retain its security policy',
|
|
6973
7676
|
)
|
|
@@ -6977,26 +7680,49 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
|
|
|
6977
7680
|
}
|
|
6978
7681
|
}
|
|
6979
7682
|
|
|
6980
|
-
` : ""}${needsShowcase ? `${EXPORT_KEYWORD}
|
|
6981
|
-
"base-uri 'none'; object-src 'none'; script-src 'self' 'unsafe-inline'; script-src-attr 'none'; style-src 'self' 'unsafe-inline'"
|
|
6982
|
-
|
|
6983
|
-
${EXPORT_KEYWORD} function showcaseHtml(): Plugin {
|
|
7683
|
+
` : ""}${needsShowcase ? `${EXPORT_KEYWORD} function showcaseHtml(): Plugin {
|
|
6984
7684
|
return {
|
|
6985
7685
|
name: 'orkestrel-showcase-html',
|
|
6986
|
-
enforce: 'post',
|
|
6987
7686
|
transformIndexHtml: {
|
|
6988
7687
|
order: 'post',
|
|
6989
7688
|
handler(html, context) {
|
|
6990
|
-
if (!
|
|
6991
|
-
|
|
6992
|
-
throw new Error('[orkestrel-showcase] Browser HTML must retain its security policy')
|
|
7689
|
+
if (!isShowcaseHtmlEntry(context.filename) || context.bundle === undefined) {
|
|
7690
|
+
return undefined
|
|
6993
7691
|
}
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
)
|
|
7692
|
+
if (!html.includes(SHOWCASE_SECURITY_META)) {
|
|
7693
|
+
throw new Error(
|
|
7694
|
+
'[orkestrel-showcase-html] Showcase build did not retain its development security policy',
|
|
7695
|
+
)
|
|
7696
|
+
}
|
|
7697
|
+
const secured = html.replace(SHOWCASE_SECURITY_META, SHOWCASE_BUILD_SECURITY_META)
|
|
7698
|
+
const build = createHash('sha256').update(secured).digest('hex')
|
|
7699
|
+
return {
|
|
7700
|
+
html: secured,
|
|
7701
|
+
tags: [
|
|
7702
|
+
{
|
|
7703
|
+
tag: 'meta',
|
|
7704
|
+
attrs: { name: 'build-id', content: build },
|
|
7705
|
+
injectTo: 'head',
|
|
7706
|
+
},
|
|
7707
|
+
],
|
|
7708
|
+
}
|
|
7709
|
+
},
|
|
7710
|
+
},
|
|
7711
|
+
generateBundle: {
|
|
7712
|
+
order: 'post',
|
|
7713
|
+
handler(_options, bundle) {
|
|
7714
|
+
let html: (typeof bundle)[string] | undefined
|
|
7715
|
+
for (const output of Object.values(bundle)) {
|
|
7716
|
+
if (!output.fileName.endsWith('.html')) continue
|
|
7717
|
+
if (html !== undefined) {
|
|
7718
|
+
this.error('[orkestrel-showcase-html] Showcase build emitted multiple HTML entries')
|
|
7719
|
+
}
|
|
7720
|
+
html = output
|
|
7721
|
+
}
|
|
7722
|
+
if (html === undefined) {
|
|
7723
|
+
this.error('[orkestrel-showcase-html] Showcase build did not emit an HTML entry')
|
|
7724
|
+
}
|
|
7725
|
+
html.fileName = 'index.html'
|
|
7000
7726
|
},
|
|
7001
7727
|
},
|
|
7002
7728
|
}
|
|
@@ -7293,7 +8019,7 @@ ${needsBrowser ? `import { isCSSRequest, parseSync, preprocessCSS, transformWith
|
|
|
7293
8019
|
`}import { defineConfig, mergeConfig } from 'vitest/config'
|
|
7294
8020
|
import tsconfig from './tsconfig.json' with { type: 'json' }
|
|
7295
8021
|
import { fileURLToPath, URL } from 'node:url'
|
|
7296
|
-
import { isBuiltin } from 'node:module'
|
|
8022
|
+
${showcaseHashImport}import { isBuiltin } from 'node:module'
|
|
7297
8023
|
import {
|
|
7298
8024
|
${needsBrowser ? " accessSync,\n" : ""} closeSync,
|
|
7299
8025
|
constants as FS_CONSTANTS,
|
|
@@ -7616,7 +8342,7 @@ ${environmentBoundary}`;
|
|
|
7616
8342
|
* ```
|
|
7617
8343
|
*/
|
|
7618
8344
|
function policyViteProject() {
|
|
7619
|
-
return `${EXPORT_KEYWORD} const policy = (
|
|
8345
|
+
return `${EXPORT_KEYWORD} const policy = (options?: UserConfig): UserConfig =>
|
|
7620
8346
|
mergeConfig(
|
|
7621
8347
|
{
|
|
7622
8348
|
resolve,
|
|
@@ -7628,7 +8354,34 @@ function policyViteProject() {
|
|
|
7628
8354
|
browser: { enabled: false },
|
|
7629
8355
|
},
|
|
7630
8356
|
},
|
|
7631
|
-
|
|
8357
|
+
options ?? {},
|
|
8358
|
+
)
|
|
8359
|
+
`;
|
|
8360
|
+
}
|
|
8361
|
+
/**
|
|
8362
|
+
* Build the standalone Node-only root-configuration Vitest project.
|
|
8363
|
+
*
|
|
8364
|
+
* @returns The emitted `config` project definition.
|
|
8365
|
+
*
|
|
8366
|
+
* @example
|
|
8367
|
+
* ```ts
|
|
8368
|
+
* configViteProject().includes("label: 'config'") // true
|
|
8369
|
+
* ```
|
|
8370
|
+
*/
|
|
8371
|
+
function configViteProject() {
|
|
8372
|
+
return `${EXPORT_KEYWORD} const config = (options?: UserConfig): UserConfig =>
|
|
8373
|
+
mergeConfig(
|
|
8374
|
+
{
|
|
8375
|
+
resolve,
|
|
8376
|
+
test: {
|
|
8377
|
+
name: { label: 'config', color: 'yellow' },
|
|
8378
|
+
include: ['tests/config/**/*.test.ts'],
|
|
8379
|
+
setupFiles: ['./tests/setup.ts'],
|
|
8380
|
+
environment: 'node',
|
|
8381
|
+
browser: { enabled: false },
|
|
8382
|
+
},
|
|
8383
|
+
},
|
|
8384
|
+
options ?? {},
|
|
7632
8385
|
)
|
|
7633
8386
|
`;
|
|
7634
8387
|
}
|
|
@@ -7643,7 +8396,7 @@ function policyViteProject() {
|
|
|
7643
8396
|
* ```
|
|
7644
8397
|
*/
|
|
7645
8398
|
function guidesViteProject() {
|
|
7646
|
-
return `${EXPORT_KEYWORD} const guides = (
|
|
8399
|
+
return `${EXPORT_KEYWORD} const guides = (options?: UserConfig): UserConfig =>
|
|
7647
8400
|
mergeConfig(
|
|
7648
8401
|
{
|
|
7649
8402
|
resolve,
|
|
@@ -7656,7 +8409,7 @@ function guidesViteProject() {
|
|
|
7656
8409
|
browser: { enabled: false },
|
|
7657
8410
|
},
|
|
7658
8411
|
},
|
|
7659
|
-
|
|
8412
|
+
options ?? {},
|
|
7660
8413
|
)
|
|
7661
8414
|
`;
|
|
7662
8415
|
}
|
|
@@ -7671,7 +8424,7 @@ function guidesViteProject() {
|
|
|
7671
8424
|
* ```
|
|
7672
8425
|
*/
|
|
7673
8426
|
function binViteProject() {
|
|
7674
|
-
return `${EXPORT_KEYWORD} const srcBin = (
|
|
8427
|
+
return `${EXPORT_KEYWORD} const srcBin = (options?: UserConfig): UserConfig =>
|
|
7675
8428
|
mergeConfig(
|
|
7676
8429
|
{
|
|
7677
8430
|
resolve,
|
|
@@ -7698,7 +8451,7 @@ function binViteProject() {
|
|
|
7698
8451
|
browser: { enabled: false },
|
|
7699
8452
|
},
|
|
7700
8453
|
},
|
|
7701
|
-
|
|
8454
|
+
options ?? {},
|
|
7702
8455
|
)
|
|
7703
8456
|
`;
|
|
7704
8457
|
}
|
|
@@ -7716,7 +8469,7 @@ function binViteProject() {
|
|
|
7716
8469
|
* ```
|
|
7717
8470
|
*/
|
|
7718
8471
|
function integrationViteProject(facts = {}) {
|
|
7719
|
-
return `${EXPORT_KEYWORD} const integration = (
|
|
8472
|
+
return `${EXPORT_KEYWORD} const integration = (options?: UserConfig): UserConfig =>
|
|
7720
8473
|
mergeConfig(
|
|
7721
8474
|
{
|
|
7722
8475
|
resolve,
|
|
@@ -7733,29 +8486,32 @@ ${facts.bin === true && facts.integration === true && facts.global === true ? `
|
|
|
7733
8486
|
fileParallelism: false,
|
|
7734
8487
|
},
|
|
7735
8488
|
},
|
|
7736
|
-
|
|
8489
|
+
options ?? {},
|
|
7737
8490
|
)
|
|
7738
8491
|
`;
|
|
7739
8492
|
}
|
|
7740
8493
|
/**
|
|
7741
|
-
* Build
|
|
8494
|
+
* Build one standalone Node-only live-service vendor proof project.
|
|
7742
8495
|
*
|
|
7743
|
-
* @
|
|
8496
|
+
* @param name - The bounded vendor directory name.
|
|
8497
|
+
* @returns The emitted `service:<name>` project definition.
|
|
7744
8498
|
*
|
|
7745
8499
|
* @example
|
|
7746
8500
|
* ```ts
|
|
7747
|
-
* serviceViteProject().includes("label: 'service'") // true
|
|
8501
|
+
* serviceViteProject('claude').includes("label: 'service:claude'") // true
|
|
7748
8502
|
* ```
|
|
7749
8503
|
*/
|
|
7750
|
-
function serviceViteProject() {
|
|
7751
|
-
|
|
8504
|
+
function serviceViteProject(name) {
|
|
8505
|
+
if (!NAME_PATTERN.test(name) || name.length > 203) throw new Error("Service project name must be a bounded lowercase directory name");
|
|
8506
|
+
return `${EXPORT_KEYWORD} const ${`service${pascalCase(name)}`} = (options?: UserConfig): UserConfig =>
|
|
7752
8507
|
mergeConfig(
|
|
7753
8508
|
{
|
|
7754
8509
|
resolve,
|
|
8510
|
+
plugins: [environmentBoundary('app/server')],
|
|
7755
8511
|
test: {
|
|
7756
|
-
name: { label:
|
|
7757
|
-
include: [
|
|
7758
|
-
setupFiles: ['./tests/setup.ts', './tests/
|
|
8512
|
+
name: { label: ${serializeTypeScriptString(`service:${name}`)}, color: 'red' },
|
|
8513
|
+
include: [${serializeTypeScriptString(`tests/service/${name}/**/*.test.ts`)}],
|
|
8514
|
+
setupFiles: ['./tests/setup.ts', './tests/setupServer.ts', ${serializeTypeScriptString(`./tests/service/${name}/setup.ts`)}],
|
|
7759
8515
|
environment: 'node',
|
|
7760
8516
|
browser: { enabled: false },
|
|
7761
8517
|
testTimeout: 120_000,
|
|
@@ -7763,7 +8519,7 @@ function serviceViteProject() {
|
|
|
7763
8519
|
fileParallelism: false,
|
|
7764
8520
|
},
|
|
7765
8521
|
},
|
|
7766
|
-
|
|
8522
|
+
options ?? {},
|
|
7767
8523
|
)
|
|
7768
8524
|
`;
|
|
7769
8525
|
}
|
|
@@ -7790,7 +8546,7 @@ function singleSrcViteConfig(environment, facts = {}) {
|
|
|
7790
8546
|
const renderedTest = renderViteTest(viteProjectRegistrations([environment], [], facts), machinery.browser);
|
|
7791
8547
|
const definitions = viteProjectDefinitions(facts);
|
|
7792
8548
|
if (environment === "browser") return `${header}
|
|
7793
|
-
${EXPORT_KEYWORD} const srcBrowser = (
|
|
8549
|
+
${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
|
|
7794
8550
|
mergeConfig(
|
|
7795
8551
|
{
|
|
7796
8552
|
resolve,
|
|
@@ -7815,7 +8571,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
7815
8571
|
name: { label: 'src:browser', color: 'yellow' },
|
|
7816
8572
|
include: ['tests/src/browser/**/*.test.ts'],
|
|
7817
8573
|
${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
7818
|
-
...(
|
|
8574
|
+
...(options?.test?.browser?.enabled === false
|
|
7819
8575
|
? {}
|
|
7820
8576
|
: {
|
|
7821
8577
|
deps: {
|
|
@@ -7835,7 +8591,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
7835
8591
|
fileParallelism: false,
|
|
7836
8592
|
},
|
|
7837
8593
|
},
|
|
7838
|
-
|
|
8594
|
+
options ?? {},
|
|
7839
8595
|
)
|
|
7840
8596
|
|
|
7841
8597
|
${definitions}
|
|
@@ -7845,7 +8601,7 @@ ${renderedTest}
|
|
|
7845
8601
|
})
|
|
7846
8602
|
`;
|
|
7847
8603
|
return `${header}
|
|
7848
|
-
${EXPORT_KEYWORD} const srcServer = (
|
|
8604
|
+
${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
|
|
7849
8605
|
mergeConfig(
|
|
7850
8606
|
{
|
|
7851
8607
|
resolve,
|
|
@@ -7875,7 +8631,7 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
|
|
|
7875
8631
|
browser: { enabled: false },
|
|
7876
8632
|
},
|
|
7877
8633
|
},
|
|
7878
|
-
|
|
8634
|
+
options ?? {},
|
|
7879
8635
|
)
|
|
7880
8636
|
|
|
7881
8637
|
${definitions}
|
|
@@ -7905,7 +8661,7 @@ ${renderedTest}
|
|
|
7905
8661
|
*
|
|
7906
8662
|
* @param src - The declared `Environment[]`.
|
|
7907
8663
|
* @param facts - Optional structural facts. `bin` appends the standalone executable
|
|
7908
|
-
* build-and-test project; `integration` and `
|
|
8664
|
+
* build-and-test project; `integration` and `services` append their standalone
|
|
7909
8665
|
* proof projects; `global` wires the shared global-setup module.
|
|
7910
8666
|
* @returns The root `vite.config.ts` file content, newline-terminated.
|
|
7911
8667
|
*
|
|
@@ -7924,7 +8680,7 @@ function rootViteConfig(src, facts = {}) {
|
|
|
7924
8680
|
if (onlyEnvironment === "browser" || onlyEnvironment === "server") return singleSrcViteConfig(onlyEnvironment, facts);
|
|
7925
8681
|
}
|
|
7926
8682
|
const browserBlock = `
|
|
7927
|
-
${EXPORT_KEYWORD} const srcBrowser = (
|
|
8683
|
+
${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
|
|
7928
8684
|
srcCore(
|
|
7929
8685
|
mergeConfig(
|
|
7930
8686
|
{
|
|
@@ -7948,7 +8704,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
7948
8704
|
include: ['tests/src/browser/**/*.test.ts'],
|
|
7949
8705
|
exclude: ['tests/src/core/**/*.test.ts'],
|
|
7950
8706
|
${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
7951
|
-
...(
|
|
8707
|
+
...(options?.test?.browser?.enabled === false
|
|
7952
8708
|
? {}
|
|
7953
8709
|
: {
|
|
7954
8710
|
deps: {
|
|
@@ -7968,12 +8724,12 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
7968
8724
|
fileParallelism: false,
|
|
7969
8725
|
},
|
|
7970
8726
|
},
|
|
7971
|
-
|
|
8727
|
+
options ?? {},
|
|
7972
8728
|
),
|
|
7973
8729
|
)
|
|
7974
8730
|
`;
|
|
7975
8731
|
const serverBlock = `
|
|
7976
|
-
${EXPORT_KEYWORD} const srcServer = (
|
|
8732
|
+
${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
|
|
7977
8733
|
srcCore(
|
|
7978
8734
|
mergeConfig(
|
|
7979
8735
|
{
|
|
@@ -8011,14 +8767,14 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
|
|
|
8011
8767
|
setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
8012
8768
|
},
|
|
8013
8769
|
},
|
|
8014
|
-
|
|
8770
|
+
options ?? {},
|
|
8015
8771
|
),
|
|
8016
8772
|
)
|
|
8017
8773
|
`;
|
|
8018
8774
|
const blocks = nonCore.map((environment) => environment === "browser" ? browserBlock : serverBlock).join("");
|
|
8019
8775
|
const renderedTest = renderViteTest(viteProjectRegistrations(src, [], facts), machinery.browser);
|
|
8020
8776
|
return `${header}
|
|
8021
|
-
${EXPORT_KEYWORD} const srcCore = (
|
|
8777
|
+
${EXPORT_KEYWORD} const srcCore = (options?: UserConfig): UserConfig =>
|
|
8022
8778
|
mergeConfig(
|
|
8023
8779
|
{
|
|
8024
8780
|
resolve,
|
|
@@ -8036,7 +8792,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
|
|
|
8036
8792
|
browser: { enabled: false },
|
|
8037
8793
|
},
|
|
8038
8794
|
},
|
|
8039
|
-
|
|
8795
|
+
options ?? {},
|
|
8040
8796
|
)
|
|
8041
8797
|
${blocks}
|
|
8042
8798
|
${viteProjectDefinitions(facts)}
|
|
@@ -8066,7 +8822,7 @@ function applicationViteConfig(src, app, facts = {}) {
|
|
|
8066
8822
|
const header = viteHeader(machinery);
|
|
8067
8823
|
const blocks = [];
|
|
8068
8824
|
if (src.includes("core")) blocks.push(`
|
|
8069
|
-
${EXPORT_KEYWORD} const srcCore = (
|
|
8825
|
+
${EXPORT_KEYWORD} const srcCore = (options?: UserConfig): UserConfig =>
|
|
8070
8826
|
mergeConfig(
|
|
8071
8827
|
{
|
|
8072
8828
|
resolve,
|
|
@@ -8081,7 +8837,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
|
|
|
8081
8837
|
browser: { enabled: false },
|
|
8082
8838
|
},
|
|
8083
8839
|
},
|
|
8084
|
-
|
|
8840
|
+
options ?? {},
|
|
8085
8841
|
)
|
|
8086
8842
|
`);
|
|
8087
8843
|
if (src.includes("browser")) {
|
|
@@ -8089,7 +8845,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
|
|
|
8089
8845
|
output: { paths: { '@src/core': '../core/index.js' } },` : "";
|
|
8090
8846
|
const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
|
|
8091
8847
|
blocks.push(`
|
|
8092
|
-
${EXPORT_KEYWORD} const srcBrowser = (
|
|
8848
|
+
${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
|
|
8093
8849
|
mergeConfig(
|
|
8094
8850
|
{
|
|
8095
8851
|
resolve,
|
|
@@ -8114,7 +8870,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
8114
8870
|
name: { label: 'src:browser', color: 'yellow' },
|
|
8115
8871
|
include: ['tests/src/browser/**/*.test.ts'],
|
|
8116
8872
|
${hasSourceCore ? "exclude: ['tests/src/core/**/*.test.ts'],\n " : ""}${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
8117
|
-
...(
|
|
8873
|
+
...(options?.test?.browser?.enabled === false
|
|
8118
8874
|
? {}
|
|
8119
8875
|
: {
|
|
8120
8876
|
deps: {
|
|
@@ -8134,7 +8890,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
8134
8890
|
fileParallelism: false,
|
|
8135
8891
|
},
|
|
8136
8892
|
},
|
|
8137
|
-
|
|
8893
|
+
options ?? {},
|
|
8138
8894
|
)
|
|
8139
8895
|
`);
|
|
8140
8896
|
}
|
|
@@ -8155,7 +8911,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
8155
8911
|
const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
|
|
8156
8912
|
const formats = hasSourceCore ? "" : "\n formats: ['es', 'cjs'],";
|
|
8157
8913
|
blocks.push(`
|
|
8158
|
-
${EXPORT_KEYWORD} const srcServer = (
|
|
8914
|
+
${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
|
|
8159
8915
|
mergeConfig(
|
|
8160
8916
|
{
|
|
8161
8917
|
resolve,
|
|
@@ -8185,12 +8941,12 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
|
|
|
8185
8941
|
browser: { enabled: false },
|
|
8186
8942
|
},
|
|
8187
8943
|
},
|
|
8188
|
-
|
|
8944
|
+
options ?? {},
|
|
8189
8945
|
)
|
|
8190
8946
|
`);
|
|
8191
8947
|
}
|
|
8192
8948
|
if (app.includes("core")) blocks.push(`
|
|
8193
|
-
${EXPORT_KEYWORD} const appCore = (
|
|
8949
|
+
${EXPORT_KEYWORD} const appCore = (options?: UserConfig): UserConfig =>
|
|
8194
8950
|
mergeConfig(
|
|
8195
8951
|
{
|
|
8196
8952
|
resolve,
|
|
@@ -8204,32 +8960,27 @@ ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
|
|
|
8204
8960
|
browser: { enabled: false },
|
|
8205
8961
|
},
|
|
8206
8962
|
},
|
|
8207
|
-
|
|
8963
|
+
options ?? {},
|
|
8208
8964
|
)
|
|
8209
8965
|
`);
|
|
8210
8966
|
if (app.includes("browser")) blocks.push(facts.showcase === true ? `
|
|
8211
|
-
${
|
|
8212
|
-
|
|
8967
|
+
${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
|
|
8968
|
+
if (config.length > 0) {
|
|
8969
|
+
throw new Error(
|
|
8970
|
+
'[orkestrel-environment-boundary] Browser configuration overrides are not permitted by the generated boundary',
|
|
8971
|
+
)
|
|
8972
|
+
}
|
|
8213
8973
|
return {
|
|
8214
8974
|
resolve,
|
|
8215
8975
|
css: ENVIRONMENT_CSS,
|
|
8216
8976
|
html: environmentHtml(),
|
|
8217
8977
|
plugins: [
|
|
8218
8978
|
restoreHtml(),
|
|
8219
|
-
outputBoundary(
|
|
8979
|
+
outputBoundary('dist/app/browser'),
|
|
8220
8980
|
environmentBoundary('app/browser'),
|
|
8221
8981
|
vue(),
|
|
8222
8982
|
prepareHtml(),
|
|
8223
8983
|
finalizeHtml(),
|
|
8224
|
-
...(showcase
|
|
8225
|
-
? [
|
|
8226
|
-
viteSingleFile({
|
|
8227
|
-
removeViteModuleLoader: true,
|
|
8228
|
-
useRecommendedBuildConfig: true,
|
|
8229
|
-
}),
|
|
8230
|
-
showcaseHtml(),
|
|
8231
|
-
]
|
|
8232
|
-
: []),
|
|
8233
8984
|
],
|
|
8234
8985
|
root: resolveWorkspacePath('app/browser'),
|
|
8235
8986
|
publicDir: false,
|
|
@@ -8240,18 +8991,9 @@ ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
|
|
|
8240
8991
|
},
|
|
8241
8992
|
},
|
|
8242
8993
|
build: {
|
|
8243
|
-
|
|
8244
|
-
? {
|
|
8245
|
-
cssMinify: 'lightningcss',
|
|
8246
|
-
minify: 'oxc',
|
|
8247
|
-
modulePreload: false,
|
|
8248
|
-
reportCompressedSize: false,
|
|
8249
|
-
sourcemap: false,
|
|
8250
|
-
target: 'esnext',
|
|
8251
|
-
}
|
|
8252
|
-
: { assetsInlineLimit: 0 }),
|
|
8994
|
+
assetsInlineLimit: 0,
|
|
8253
8995
|
emptyOutDir: true,
|
|
8254
|
-
outDir: resolveWorkspacePath(
|
|
8996
|
+
outDir: resolveWorkspacePath('dist/app/browser'),
|
|
8255
8997
|
rolldownOptions: {
|
|
8256
8998
|
input: resolveWorkspacePath('${APP_MATRIX.browser.entry}'),
|
|
8257
8999
|
},
|
|
@@ -8280,22 +9022,54 @@ ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
|
|
|
8280
9022
|
}
|
|
8281
9023
|
}
|
|
8282
9024
|
|
|
8283
|
-
${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
|
|
8284
|
-
if (config.length > 0) {
|
|
8285
|
-
throw new Error(
|
|
8286
|
-
'[orkestrel-environment-boundary] Browser configuration overrides are not permitted by the generated boundary',
|
|
8287
|
-
)
|
|
8288
|
-
}
|
|
8289
|
-
return applicationBrowser(false)
|
|
8290
|
-
}
|
|
8291
|
-
|
|
8292
9025
|
${EXPORT_KEYWORD} function appShowcase(...config: never[]): UserConfig {
|
|
8293
9026
|
if (config.length > 0) {
|
|
8294
9027
|
throw new Error(
|
|
8295
9028
|
'[orkestrel-environment-boundary] Showcase configuration overrides are not permitted by the generated boundary',
|
|
8296
9029
|
)
|
|
8297
9030
|
}
|
|
8298
|
-
return
|
|
9031
|
+
return {
|
|
9032
|
+
base: './',
|
|
9033
|
+
resolve,
|
|
9034
|
+
css: ENVIRONMENT_CSS,
|
|
9035
|
+
html: environmentHtml(),
|
|
9036
|
+
plugins: [
|
|
9037
|
+
restoreHtml(),
|
|
9038
|
+
outputBoundary('dist/showcase'),
|
|
9039
|
+
environmentBoundary('app/browser'),
|
|
9040
|
+
vue(),
|
|
9041
|
+
prepareHtml(),
|
|
9042
|
+
showcaseHtml(),
|
|
9043
|
+
viteSingleFile({
|
|
9044
|
+
removeViteModuleLoader: true,
|
|
9045
|
+
useRecommendedBuildConfig: true,
|
|
9046
|
+
}),
|
|
9047
|
+
finalizeHtml(),
|
|
9048
|
+
],
|
|
9049
|
+
root: resolveWorkspacePath('app/browser'),
|
|
9050
|
+
publicDir: false,
|
|
9051
|
+
server: {
|
|
9052
|
+
open: '/showcase.html',
|
|
9053
|
+
fs: {
|
|
9054
|
+
strict: true,
|
|
9055
|
+
allow: [...browserServerRoots()],
|
|
9056
|
+
},
|
|
9057
|
+
},
|
|
9058
|
+
build: {
|
|
9059
|
+
assetsInlineLimit: Number.MAX_SAFE_INTEGER,
|
|
9060
|
+
cssMinify: 'lightningcss',
|
|
9061
|
+
emptyOutDir: true,
|
|
9062
|
+
minify: 'oxc',
|
|
9063
|
+
modulePreload: false,
|
|
9064
|
+
outDir: resolveWorkspacePath('dist/showcase'),
|
|
9065
|
+
reportCompressedSize: false,
|
|
9066
|
+
rolldownOptions: {
|
|
9067
|
+
input: resolveWorkspacePath('app/browser/showcase.html'),
|
|
9068
|
+
},
|
|
9069
|
+
sourcemap: false,
|
|
9070
|
+
target: 'esnext',
|
|
9071
|
+
},
|
|
9072
|
+
}
|
|
8299
9073
|
}
|
|
8300
9074
|
` : `
|
|
8301
9075
|
${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
|
|
@@ -8357,7 +9131,7 @@ ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
|
|
|
8357
9131
|
}
|
|
8358
9132
|
`);
|
|
8359
9133
|
if (app.includes("server")) blocks.push(`
|
|
8360
|
-
${EXPORT_KEYWORD} const appServer = (
|
|
9134
|
+
${EXPORT_KEYWORD} const appServer = (options?: UserConfig): UserConfig =>
|
|
8361
9135
|
mergeConfig(
|
|
8362
9136
|
{
|
|
8363
9137
|
resolve,
|
|
@@ -8384,7 +9158,7 @@ ${EXPORT_KEYWORD} const appServer = (config?: UserConfig): UserConfig =>
|
|
|
8384
9158
|
browser: { enabled: false },
|
|
8385
9159
|
},
|
|
8386
9160
|
},
|
|
8387
|
-
|
|
9161
|
+
options ?? {},
|
|
8388
9162
|
)
|
|
8389
9163
|
`);
|
|
8390
9164
|
const renderedTest = renderViteTest(viteProjectRegistrations(src, app, facts), machinery.browser);
|
|
@@ -8619,6 +9393,8 @@ function appTsconfig(environment, hasCore) {
|
|
|
8619
9393
|
const include = TYPESCRIPT_EXTENSIONS.map((extension) => `../../app/${environment}/**/*.${extension}`);
|
|
8620
9394
|
if (environment === "browser") include.push(`../../app/${environment}/**/*.vue`);
|
|
8621
9395
|
if (environment !== "core" && hasCore) include.push(...TYPESCRIPT_EXTENSIONS.map((extension) => `../../app/core/**/*.${extension}`));
|
|
9396
|
+
include.push(...TYPESCRIPT_EXTENSIONS.map((extension) => `../../tests/app/${environment}/**/*.${extension}`));
|
|
9397
|
+
include.push(environment === "browser" ? "../../tests/setupBrowser.ts" : environment === "server" ? "../../tests/setupServer.ts" : "../../tests/setup.ts");
|
|
8622
9398
|
return formatJson({
|
|
8623
9399
|
extends: "../../tsconfig.json",
|
|
8624
9400
|
compilerOptions: {
|
|
@@ -8665,7 +9441,7 @@ function ciWorkflow(spec) {
|
|
|
8665
9441
|
const tail = [];
|
|
8666
9442
|
if (spec.integration) tail.push(` - name: Run live consumer integration
|
|
8667
9443
|
run: npm run test:integration`);
|
|
8668
|
-
if (spec.
|
|
9444
|
+
if (spec.services.length > 0) {
|
|
8669
9445
|
tail.push(` - name: Provision live service
|
|
8670
9446
|
run: bash ${SERVICE_SCRIPT_PATH}`);
|
|
8671
9447
|
tail.push(` - name: Run live service tests
|
|
@@ -8799,7 +9575,7 @@ export default defineConfig(appShowcase())
|
|
|
8799
9575
|
* factory per environment (AGENTS §5's per-environment centralized-file pattern), so
|
|
8800
9576
|
* every environment gets the same uniform stub shape.
|
|
8801
9577
|
*
|
|
8802
|
-
* @param spec - The
|
|
9578
|
+
* @param spec - The blueprint carrying the declared source environment set.
|
|
8803
9579
|
* @param pascal - The package's PascalCase entity name.
|
|
8804
9580
|
* @returns The `source` group's `Artifact[]`.
|
|
8805
9581
|
*
|
|
@@ -8832,20 +9608,129 @@ function sourceArtifacts(spec, pascal) {
|
|
|
8832
9608
|
* Draft the application source artifacts for every selected app environment.
|
|
8833
9609
|
*
|
|
8834
9610
|
* @param spec - The blueprint carrying the application environment set.
|
|
9611
|
+
* @remarks
|
|
9612
|
+
* Two conditional shapes layer over the per-environment set. The health contract —
|
|
9613
|
+
* record, route constants, guard, and the one unknown-to-typed read — is declared by
|
|
9614
|
+
* `app/server` while the server alone reads it and RELOCATES to `app/core` the moment
|
|
9615
|
+
* the browser reads it too, because a contract two hosts share belongs to neither of
|
|
9616
|
+
* them. The showcase entry pair, its seeder, and its factory appear only for a
|
|
9617
|
+
* blueprint that declares the physical showcase wrapper alongside `app/browser`.
|
|
8835
9618
|
* @returns Complete, runnable app/core, app/browser, and app/server artifacts.
|
|
8836
9619
|
*/
|
|
8837
9620
|
function applicationArtifacts(spec) {
|
|
8838
9621
|
const artifacts = [];
|
|
8839
9622
|
const hasCore = spec.app.includes("core");
|
|
9623
|
+
const hasBrowser = spec.app.includes("browser");
|
|
9624
|
+
const hasBoundary = hasApplicationBoundary(spec);
|
|
9625
|
+
const hasShowcase = hasApplicationShowcase(spec);
|
|
9626
|
+
const showcaseSource = hasBoundary ? "its running server" : "its own configuration";
|
|
8840
9627
|
const nameLiteral = serializeTypeScriptString(spec.name);
|
|
8841
|
-
|
|
8842
|
-
|
|
9628
|
+
const sharedRecord = `
|
|
9629
|
+
/** The application record both hosts read at the health route. */
|
|
9630
|
+
${EXPORT_KEYWORD} interface ApplicationRecord {
|
|
9631
|
+
readonly name: string
|
|
9632
|
+
readonly status: 'ok'
|
|
9633
|
+
}
|
|
9634
|
+
`;
|
|
9635
|
+
const healthConstants = `
|
|
9636
|
+
/** The only HTTP method owned by the application health route. */
|
|
9637
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_METHOD = 'GET'
|
|
9638
|
+
|
|
9639
|
+
/** The only HTTP path owned by the generated application server. */
|
|
9640
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/health'
|
|
9641
|
+
`;
|
|
9642
|
+
if (hasCore) {
|
|
9643
|
+
artifacts.push(fillArtifact("app/core/types.ts", "source", "appCoreTypes", { record: hasBoundary ? sharedRecord : "" }, "core"), fillArtifact("app/core/constants.ts", "source", "appCoreConstants", {
|
|
9644
|
+
nameLiteral,
|
|
9645
|
+
health: hasBoundary ? `${healthConstants}
|
|
9646
|
+
/** Milliseconds allowed for one shared application health read. */
|
|
9647
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_TIMEOUT = 5_000
|
|
9648
|
+
` : ""
|
|
9649
|
+
}, "core"), fillArtifact("app/core/errors.ts", "source", "appCoreErrors", {}, "core"), fillArtifact("app/core/parsers.ts", "source", "appCoreParsers", {}, "core"), fillArtifact("app/core/factories.ts", "source", "appCoreFactories", {}, "core"), fillArtifact("app/core/index.ts", "source", "appCoreIndex", {
|
|
9650
|
+
validators: hasBoundary ? "export * from './validators.js'\n" : "",
|
|
9651
|
+
handlers: hasBoundary ? "export * from './handlers.js'\n" : ""
|
|
9652
|
+
}, "core"));
|
|
9653
|
+
if (hasBoundary) artifacts.push(fillArtifact("app/core/validators.ts", "source", "appCoreValidators", {}, "core"), fillArtifact("app/core/handlers.ts", "source", "appCoreHandlers", {}, "core"));
|
|
9654
|
+
}
|
|
9655
|
+
if (hasBrowser) {
|
|
8843
9656
|
const nameImport = hasCore ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from './constants.js'";
|
|
8844
9657
|
const nameConstant = hasCore ? "" : `/** The browser-only application name. */
|
|
8845
9658
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
|
|
8846
9659
|
|
|
8847
9660
|
`;
|
|
8848
|
-
artifacts.push(fillArtifact("app/browser/types.ts", "source", "appBrowserTypes", {
|
|
9661
|
+
artifacts.push(fillArtifact("app/browser/types.ts", "source", "appBrowserTypes", { application: hasShowcase && !hasCore ? `
|
|
9662
|
+
/** The identity the root view renders. */
|
|
9663
|
+
${EXPORT_KEYWORD} interface Application {
|
|
9664
|
+
readonly name: string
|
|
9665
|
+
}
|
|
9666
|
+
` : "" }, "browser"), fillArtifact("app/browser/constants.ts", "source", "appBrowserConstants", { nameConstant }, "browser"), fillArtifact("app/browser/errors.ts", "source", "appBrowserErrors", {}, "browser"), fillArtifact("app/browser/parsers.ts", "source", "appBrowserParsers", {}, "browser"), fillArtifact("app/browser/factories.ts", "source", "appBrowserFactories", {
|
|
9667
|
+
nameImport: hasBoundary ? "import { APP_NAME, readApplicationHealth } from '@app/core'" : nameImport,
|
|
9668
|
+
seedImport: hasShowcase ? "import { seedApplication } from './seeders.js'\n" : "",
|
|
9669
|
+
showcase: hasShowcase ? `
|
|
9670
|
+
/**
|
|
9671
|
+
* Mount the showcase over its seeded, inert identity.
|
|
9672
|
+
*
|
|
9673
|
+
* @param target - The browser element or selector that receives the showcase.
|
|
9674
|
+
* @returns The mounted Vue application.
|
|
9675
|
+
*
|
|
9676
|
+
* @remarks
|
|
9677
|
+
* The showcase mounts the same {@link createBrowserApplication} root the shipped entry
|
|
9678
|
+
* mounts, so the two differ in exactly one expression — where the props come from. This
|
|
9679
|
+
* one reads {@link seedApplication}; the application reads ${showcaseSource}.
|
|
9680
|
+
*
|
|
9681
|
+
* @example
|
|
9682
|
+
* \`\`\`ts
|
|
9683
|
+
* import { mountShowcaseApplication } from '@app/browser'
|
|
9684
|
+
*
|
|
9685
|
+
* mountShowcaseApplication('#app')
|
|
9686
|
+
* \`\`\`
|
|
9687
|
+
*/
|
|
9688
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} mountShowcaseApplication(target: string | Element): App<Element> {
|
|
9689
|
+
const seed = seedApplication()
|
|
9690
|
+
const application = createBrowserApplication({ name: seed.name })
|
|
9691
|
+
application.mount(target)
|
|
9692
|
+
return application
|
|
9693
|
+
}
|
|
9694
|
+
` : "",
|
|
9695
|
+
boundary: hasBoundary ? `
|
|
9696
|
+
/**
|
|
9697
|
+
* Mount the application over its real server boundary.
|
|
9698
|
+
*
|
|
9699
|
+
* @param target - The browser element or selector that receives the application.
|
|
9700
|
+
* @returns The mounted Vue application, after one health read settles.
|
|
9701
|
+
*
|
|
9702
|
+
* @remarks
|
|
9703
|
+
* One health read runs before the mount, so the root view renders the identity the
|
|
9704
|
+
* running server reported. An unreachable or off-contract boundary yields \`undefined\`
|
|
9705
|
+
* and the application falls back to its own configuration rather than failing to mount.
|
|
9706
|
+
*
|
|
9707
|
+
* @example
|
|
9708
|
+
* \`\`\`ts
|
|
9709
|
+
* import { mountBrowserApplication } from '@app/browser'
|
|
9710
|
+
*
|
|
9711
|
+
* await mountBrowserApplication('#app')
|
|
9712
|
+
* \`\`\`
|
|
9713
|
+
*/
|
|
9714
|
+
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} mountBrowserApplication(target: string | Element): Promise<App<Element>> {
|
|
9715
|
+
const seed = (await readApplicationHealth(window.location.origin)) ?? { name: APP_NAME }
|
|
9716
|
+
const application = createBrowserApplication({ name: seed.name })
|
|
9717
|
+
application.mount(target)
|
|
9718
|
+
return application
|
|
9719
|
+
}
|
|
9720
|
+
` : ""
|
|
9721
|
+
}, "browser"), fillArtifact("app/browser/index.ts", "source", "appBrowserIndex", { seeders: hasShowcase ? "export * from './seeders.js'\n" : "" }, "browser"), fillArtifact("app/browser/main.ts", "source", "appBrowserMain", hasBoundary ? {
|
|
9722
|
+
factory: "mountBrowserApplication",
|
|
9723
|
+
mount: `void mountBrowserApplication('#app').catch(() => {
|
|
9724
|
+
console.error('[ERROR] Browser application failed')
|
|
9725
|
+
})`
|
|
9726
|
+
} : {
|
|
9727
|
+
factory: "createBrowserApplication",
|
|
9728
|
+
mount: "createBrowserApplication().mount('#app')"
|
|
9729
|
+
}, "browser"), fillArtifact("app/browser/ApplicationView.vue", "source", "appBrowserView", {}, "browser"), fillArtifact("app/browser/index.html", "source", "appBrowserHtml", { name: escapeHtmlText(spec.name) }, "browser"), fillArtifact("app/browser/env.d.ts", "source", "appBrowserEnv", {}, "browser"));
|
|
9730
|
+
if (hasShowcase) artifacts.push(fillArtifact("app/browser/seeders.ts", "source", "appBrowserSeeders", {
|
|
9731
|
+
applicationImport: hasCore ? "import type { Application } from '@app/core'" : "import type { Application } from './types.js'",
|
|
9732
|
+
nameImport
|
|
9733
|
+
}, "browser"), fillArtifact("app/browser/showcase.ts", "source", "appBrowserShowcase", {}, "browser"), fillArtifact("app/browser/showcase.html", "source", "appBrowserShowcaseHtml", { name: escapeHtmlText(spec.name) }, "browser"));
|
|
8849
9734
|
}
|
|
8850
9735
|
if (spec.app.includes("server")) {
|
|
8851
9736
|
const nameImport = hasCore ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from './constants.js'";
|
|
@@ -8853,7 +9738,19 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
|
|
|
8853
9738
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
|
|
8854
9739
|
|
|
8855
9740
|
`;
|
|
8856
|
-
artifacts.push(fillArtifact("app/server/types.ts", "source", "appServerTypes", {
|
|
9741
|
+
artifacts.push(fillArtifact("app/server/types.ts", "source", "appServerTypes", { record: hasBoundary ? "" : `/** The application record returned by the health route. */
|
|
9742
|
+
${EXPORT_KEYWORD} interface ApplicationRecord {
|
|
9743
|
+
readonly name: string
|
|
9744
|
+
readonly status: 'ok'
|
|
9745
|
+
}
|
|
9746
|
+
|
|
9747
|
+
` }, "server"), fillArtifact("app/server/constants.ts", "source", "appServerConstants", {
|
|
9748
|
+
nameConstant,
|
|
9749
|
+
health: hasBoundary ? "" : healthConstants
|
|
9750
|
+
}, "server"), fillArtifact("app/server/errors.ts", "source", "appServerErrors", {}, "server"), fillArtifact("app/server/parsers.ts", "source", "appServerParsers", {}, "server"), fillArtifact("app/server/routes.ts", "source", "appServerRoutes", { healthImport: hasBoundary ? "import { APP_HEALTH_METHOD, APP_HEALTH_PATH } from '@app/core'" : "import { APP_HEALTH_METHOD, APP_HEALTH_PATH } from './constants.js'" }, "server"), fillArtifact("app/server/handlers.ts", "source", "appServerHandlers", {
|
|
9751
|
+
recordImport: hasBoundary ? "import type { ApplicationRecord } from '@app/core'" : "import type { ApplicationRecord } from './types.js'",
|
|
9752
|
+
nameImport
|
|
9753
|
+
}, "server"), fillArtifact("app/server/ApplicationServer.ts", "source", "appServerEntity", {}, "server"), fillArtifact("app/server/factories.ts", "source", "appServerFactories", {}, "server"), fillArtifact("app/server/ApplicationServerRunner.ts", "source", "appServerRunner", { nameImport }, "server"), fillArtifact("app/server/index.ts", "source", "appServerIndex", {}, "server"), fillArtifact("app/server/main.ts", "source", "appServerMain", {}, "server"));
|
|
8857
9754
|
}
|
|
8858
9755
|
return artifacts;
|
|
8859
9756
|
}
|
|
@@ -8926,7 +9823,7 @@ function paritySpecifiers(spec) {
|
|
|
8926
9823
|
function testArtifacts(spec, pascal) {
|
|
8927
9824
|
const hasBrowser = spec.src.includes("browser") || spec.app.includes("browser");
|
|
8928
9825
|
const hasVue = spec.app.includes("browser");
|
|
8929
|
-
const
|
|
9826
|
+
const machinery = viteMachinery(spec.src, spec.app, spec.bin, spec.showcase);
|
|
8930
9827
|
const vuePolicyImport = hasVue ? "\nimport { parse as parseVue } from 'vue/compiler-sfc'" : "";
|
|
8931
9828
|
const workspacePolicyAssertion = hasVue ? `expect(
|
|
8932
9829
|
inspectCodingWorkspace(process.cwd(), (path, content) => {
|
|
@@ -8937,37 +9834,219 @@ function testArtifacts(spec, pascal) {
|
|
|
8937
9834
|
)
|
|
8938
9835
|
}),
|
|
8939
9836
|
).toEqual([])` : "expect(inspectCodingWorkspace(process.cwd())).toEqual([])";
|
|
8940
|
-
const
|
|
9837
|
+
const configNames = [
|
|
9838
|
+
"containedPath",
|
|
9839
|
+
"environmentPathError",
|
|
9840
|
+
"environmentSourceError",
|
|
9841
|
+
"resolveWorkspacePath",
|
|
9842
|
+
"workspacePath"
|
|
9843
|
+
];
|
|
9844
|
+
if (machinery.output) configNames.push("enforceOutputPath");
|
|
9845
|
+
if (hasBrowser) configNames.push("isBrowserExecutable", "resolveBrowser", "resolveManagedBrowser", "resolveSystemBrowser", "SYSTEM_BROWSER_CHANNELS");
|
|
9846
|
+
if (hasVue) configNames.push("hasSecurityPrologue", "HTML_SECURITY_META", "HTML_SECURITY_POLICY");
|
|
9847
|
+
const configImports = [
|
|
9848
|
+
...hasVue ? ["import { readFileSync } from 'node:fs'"] : [],
|
|
9849
|
+
...hasBrowser ? ["import { chromium } from 'playwright'"] : [],
|
|
9850
|
+
"import { describe, expect, it } from 'vitest'",
|
|
9851
|
+
"import {",
|
|
9852
|
+
...configNames.map((name) => `\t${name},`),
|
|
9853
|
+
"} from '../../vite.config.js'"
|
|
9854
|
+
].join("\n");
|
|
9855
|
+
const configCases = [];
|
|
9856
|
+
if (machinery.output) configCases.push(`
|
|
9857
|
+
|
|
9858
|
+
it('contains build output in its exact workspace directory', () => {
|
|
9859
|
+
const expected = resolveWorkspacePath('dist/config-proof')
|
|
9860
|
+
|
|
9861
|
+
expect(() => enforceOutputPath(expected, expected)).not.toThrow()
|
|
9862
|
+
expect(() => enforceOutputPath(resolveWorkspacePath('dist/other'), expected)).toThrow(
|
|
9863
|
+
'exact configured workspace directory',
|
|
9864
|
+
)
|
|
9865
|
+
expect(() =>
|
|
9866
|
+
enforceOutputPath(
|
|
9867
|
+
resolveWorkspacePath('../config-proof'),
|
|
9868
|
+
resolveWorkspacePath('../config-proof'),
|
|
9869
|
+
),
|
|
9870
|
+
).toThrow('remain inside the workspace')
|
|
9871
|
+
})`);
|
|
9872
|
+
if (hasBrowser) configCases.push(`
|
|
8941
9873
|
|
|
8942
9874
|
it('resolves only a real managed executable or stable system browser channel', () => {
|
|
8943
|
-
const
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
9875
|
+
const pinned = chromium.executablePath()
|
|
9876
|
+
const managed = resolveManagedBrowser(pinned)
|
|
9877
|
+
const channel = resolveSystemBrowser(process.platform, process.env)
|
|
9878
|
+
const options = resolveBrowser(pinned, process.platform, process.env)
|
|
9879
|
+
const expected =
|
|
9880
|
+
managed === undefined
|
|
9881
|
+
? channel === undefined
|
|
9882
|
+
? undefined
|
|
9883
|
+
: { launchOptions: { channel } }
|
|
9884
|
+
: managed === pinned
|
|
9885
|
+
? {}
|
|
9886
|
+
: { launchOptions: { executablePath: managed } }
|
|
9887
|
+
const executable = managed === undefined || isBrowserExecutable(managed)
|
|
9888
|
+
const stable =
|
|
9889
|
+
managed !== undefined ||
|
|
9890
|
+
channel === undefined ||
|
|
9891
|
+
SYSTEM_BROWSER_CHANNELS.some((browser) => browser.channel === channel)
|
|
9892
|
+
|
|
9893
|
+
expect(options).toEqual(expected)
|
|
9894
|
+
expect(executable).toBe(true)
|
|
9895
|
+
expect(stable).toBe(true)
|
|
9896
|
+
})`);
|
|
9897
|
+
if (hasVue) configCases.push(`
|
|
9898
|
+
|
|
9899
|
+
it('preserves the generated browser security prologue', () => {
|
|
9900
|
+
const document = readFileSync(resolveWorkspacePath('app/browser/index.html'), 'utf8')
|
|
9901
|
+
|
|
9902
|
+
expect(hasSecurityPrologue(document, HTML_SECURITY_META)).toBe(true)
|
|
9903
|
+
expect(HTML_SECURITY_META).toContain(HTML_SECURITY_POLICY)
|
|
9904
|
+
})`);
|
|
9905
|
+
const artifacts = [
|
|
9906
|
+
fillArtifact("tests/setup.ts", "tests", "setup", {
|
|
9907
|
+
eventImport: spec.app.includes("server") ? "import type { EmitterInterface, EventMap } from '@orkestrel/emitter'\n\n" : "",
|
|
9908
|
+
eventHelper: spec.app.includes("server") ? `
|
|
9909
|
+
/** Wait for one typed event occurrence and return its argument tuple. */
|
|
9910
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} waitForEvent<TMap extends EventMap, K extends keyof TMap>(
|
|
9911
|
+
emitter: EmitterInterface<TMap>,
|
|
9912
|
+
event: K,
|
|
9913
|
+
): Promise<TMap[K]> {
|
|
9914
|
+
return new Promise((resolvePromise) => emitter.once(event, (...args) => resolvePromise(args)))
|
|
9915
|
+
}
|
|
9916
|
+
` : ""
|
|
9917
|
+
}),
|
|
9918
|
+
fillArtifact("tests/policy.test.ts", "tests", "policyTest", {
|
|
9919
|
+
vuePolicyImport,
|
|
9920
|
+
workspacePolicyAssertion
|
|
9921
|
+
}),
|
|
9922
|
+
fillArtifact("tests/config/vite.test.ts", "tests", "configTest", {
|
|
9923
|
+
imports: configImports,
|
|
9924
|
+
cases: configCases.join("")
|
|
9925
|
+
})
|
|
9926
|
+
];
|
|
9927
|
+
if (spec.src.includes("server") || spec.app.includes("server") || spec.services.length > 0) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
|
|
9928
|
+
if (spec.services.length > 0) artifacts.push(fillArtifact("tests/config/services.test.ts", "tests", "serviceConformance", { services: renderStringArray(spec.services, " ", "const declared = ", "") }, "server"));
|
|
8962
9929
|
if (spec.src.includes("browser") || spec.app.includes("browser")) artifacts.push(fillArtifact("tests/setupBrowser.ts", "tests", "setupBrowser", {}, "browser"));
|
|
8963
|
-
|
|
9930
|
+
const hasBoundary = hasApplicationBoundary(spec);
|
|
9931
|
+
const hasShowcase = hasApplicationShowcase(spec);
|
|
9932
|
+
if (spec.app.includes("core")) artifacts.push(fillArtifact("tests/app/core/factories.test.ts", "tests", "appCoreTest", {
|
|
9933
|
+
guardImport: hasBoundary ? " isApplicationRecord,\n" : "",
|
|
9934
|
+
readImport: hasBoundary ? " readApplicationHealth,\n" : "",
|
|
9935
|
+
boundary: hasBoundary ? `
|
|
9936
|
+
|
|
9937
|
+
describe('shared application health boundary', () => {
|
|
9938
|
+
it('accepts the shared record and refuses every off-contract value', () => {
|
|
9939
|
+
expect(isApplicationRecord({ name: APP_NAME, status: 'ok' })).toBe(true)
|
|
9940
|
+
expect(isApplicationRecord({ name: ' ', status: 'ok' })).toBe(true)
|
|
9941
|
+
for (const value of [
|
|
9942
|
+
null,
|
|
9943
|
+
[],
|
|
9944
|
+
'ok',
|
|
9945
|
+
{ name: APP_NAME },
|
|
9946
|
+
{ name: '', status: 'ok' },
|
|
9947
|
+
{ name: 1, status: 'ok' },
|
|
9948
|
+
{ name: APP_NAME, status: 'down' },
|
|
9949
|
+
]) {
|
|
9950
|
+
expect(isApplicationRecord(value)).toBe(false)
|
|
9951
|
+
}
|
|
9952
|
+
const revocable = Proxy.revocable({}, {})
|
|
9953
|
+
revocable.revoke()
|
|
9954
|
+
expect(isApplicationRecord(revocable.proxy)).toBe(false)
|
|
9955
|
+
})
|
|
9956
|
+
|
|
9957
|
+
it('refuses a malformed origin before reaching the network', async () => {
|
|
9958
|
+
expect(await readApplicationHealth('not-an-origin')).toBeUndefined()
|
|
9959
|
+
})
|
|
9960
|
+
})` : ""
|
|
9961
|
+
}, "core"));
|
|
8964
9962
|
if (spec.app.includes("browser")) {
|
|
8965
9963
|
const browserTestNameImport = spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/browser'";
|
|
8966
|
-
artifacts.push(fillArtifact("tests/app/browser/factories.test.ts", "tests", "appBrowserTest", {
|
|
9964
|
+
artifacts.push(fillArtifact("tests/app/browser/factories.test.ts", "tests", "appBrowserTest", {
|
|
9965
|
+
browserTestNameImport,
|
|
9966
|
+
showcaseImport: hasShowcase ? " mountShowcaseApplication,\n" : "",
|
|
9967
|
+
entryImport: `${hasShowcase ? " seedApplication,\n" : ""}${hasBoundary ? " mountBrowserApplication,\n" : ""}`,
|
|
9968
|
+
showcase: hasShowcase ? `
|
|
9969
|
+
|
|
9970
|
+
describe('mountShowcaseApplication', () => {
|
|
9971
|
+
it('mounts the shipped root view over one frozen, inert seed', () => {
|
|
9972
|
+
const element = buildElement()
|
|
9973
|
+
const seeded = seedApplication()
|
|
9974
|
+
const application = mountShowcaseApplication(element)
|
|
9975
|
+
try {
|
|
9976
|
+
expect(element.textContent).toContain(seeded.name)
|
|
9977
|
+
expect(seeded).toEqual(seedApplication())
|
|
9978
|
+
expect(seeded).not.toBe(seedApplication())
|
|
9979
|
+
expect(Object.isFrozen(seeded)).toBe(true)
|
|
9980
|
+
} finally {
|
|
9981
|
+
application.unmount()
|
|
9982
|
+
element.remove()
|
|
9983
|
+
}
|
|
9984
|
+
})
|
|
9985
|
+
})` : "",
|
|
9986
|
+
boundary: hasBoundary ? `
|
|
9987
|
+
|
|
9988
|
+
describe('mountBrowserApplication', () => {
|
|
9989
|
+
it('mounts the configured identity when the boundary answers off-contract', async () => {
|
|
9990
|
+
const element = buildElement()
|
|
9991
|
+
const application = await mountBrowserApplication(element)
|
|
9992
|
+
try {
|
|
9993
|
+
expect(element.textContent).toContain(APP_NAME)
|
|
9994
|
+
} finally {
|
|
9995
|
+
application.unmount()
|
|
9996
|
+
element.remove()
|
|
9997
|
+
}
|
|
9998
|
+
})
|
|
9999
|
+
})` : ""
|
|
10000
|
+
}, "browser"));
|
|
8967
10001
|
}
|
|
8968
10002
|
if (spec.app.includes("server")) {
|
|
8969
|
-
const testNameImport =
|
|
8970
|
-
|
|
10003
|
+
const testNameImport = hasBoundary ? `import {
|
|
10004
|
+
APP_HEALTH_METHOD,
|
|
10005
|
+
APP_HEALTH_PATH,
|
|
10006
|
+
APP_NAME,
|
|
10007
|
+
isApplicationRecord,
|
|
10008
|
+
readApplicationHealth,
|
|
10009
|
+
} from '@app/core'` : spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/server'";
|
|
10010
|
+
const serverImport = hasBoundary ? `import {
|
|
10011
|
+
ApplicationServerRunner,
|
|
10012
|
+
createApplicationDispatcher,
|
|
10013
|
+
createApplicationServer,
|
|
10014
|
+
} from '@app/server'` : `import {
|
|
10015
|
+
APP_HEALTH_METHOD,
|
|
10016
|
+
APP_HEALTH_PATH,
|
|
10017
|
+
ApplicationServerRunner,
|
|
10018
|
+
createApplicationDispatcher,
|
|
10019
|
+
createApplicationServer,
|
|
10020
|
+
} from '@app/server'`;
|
|
10021
|
+
artifacts.push(fillArtifact("tests/app/server/ApplicationServer.test.ts", "tests", "appServerTest", {
|
|
10022
|
+
testNameImport,
|
|
10023
|
+
serverImport,
|
|
10024
|
+
boundary: hasBoundary ? `
|
|
10025
|
+
|
|
10026
|
+
describe('shared application boundary', () => {
|
|
10027
|
+
it('answers the shared record and translates it into the shared identity', async () => {
|
|
10028
|
+
const server = createApplicationServer({ server: { host: '127.0.0.1', port: 0 } })
|
|
10029
|
+
try {
|
|
10030
|
+
await server.start()
|
|
10031
|
+
const url = server.url
|
|
10032
|
+
if (url === undefined) throw new Error('Expected a bound application URL')
|
|
10033
|
+
const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
|
|
10034
|
+
const record: unknown = await response.json()
|
|
10035
|
+
|
|
10036
|
+
expect(isApplicationRecord(record)).toBe(true)
|
|
10037
|
+
expect(await readApplicationHealth(url)).toEqual({ name: APP_NAME })
|
|
10038
|
+
} finally {
|
|
10039
|
+
await server.destroy()
|
|
10040
|
+
}
|
|
10041
|
+
})
|
|
10042
|
+
|
|
10043
|
+
it('reads undefined from a released loopback port', async () => {
|
|
10044
|
+
const port = await reserveLoopbackPort()
|
|
10045
|
+
|
|
10046
|
+
expect(await readApplicationHealth(\`http://127.0.0.1:\${port}\`)).toBeUndefined()
|
|
10047
|
+
})
|
|
10048
|
+
})` : ""
|
|
10049
|
+
}, "server"), fillArtifact("tests/app/server/parsers.test.ts", "tests", "appServerParsersTest", {}, "server"));
|
|
8971
10050
|
}
|
|
8972
10051
|
const inlineExplicitInstance = `instance: ${pascal}Interface = new ${pascal}({ id: 'example' })`;
|
|
8973
10052
|
const multilineExplicitInstance = `instance: ${pascal}Interface = new ${pascal}({
|
|
@@ -9011,12 +10090,12 @@ function testArtifacts(spec, pascal) {
|
|
|
9011
10090
|
}
|
|
9012
10091
|
artifacts.push(fillArtifact("tests/setupGuides.ts", "tests", "setupGuides", {
|
|
9013
10092
|
specifiers: paritySpecifiers(spec),
|
|
9014
|
-
walkDirs: [
|
|
9015
|
-
...spec.src.length > 0 ? ["
|
|
9016
|
-
...spec.app.length > 0 ? ["
|
|
9017
|
-
"
|
|
9018
|
-
"
|
|
9019
|
-
]
|
|
10093
|
+
walkDirs: renderStringArray([
|
|
10094
|
+
...spec.src.length > 0 ? ["src"] : [],
|
|
10095
|
+
...spec.app.length > 0 ? ["app"] : [],
|
|
10096
|
+
"guides",
|
|
10097
|
+
"tests"
|
|
10098
|
+
], "", "export const GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze(", ")")
|
|
9020
10099
|
}), fillArtifact("tests/guides/src/parity.test.ts", "tests", "parityTest", { name: spec.name }));
|
|
9021
10100
|
return artifacts;
|
|
9022
10101
|
}
|
|
@@ -9065,6 +10144,8 @@ function guideMemberTable(category, members) {
|
|
|
9065
10144
|
*/
|
|
9066
10145
|
function guideUsage(spec, pascal) {
|
|
9067
10146
|
const examples = [];
|
|
10147
|
+
const hasBoundary = hasApplicationBoundary(spec);
|
|
10148
|
+
const hasShowcase = hasApplicationShowcase(spec);
|
|
9068
10149
|
if (spec.src.length > 0) examples.push(`\`\`\`ts
|
|
9069
10150
|
import { create${pascal} } from '@orkestrel/${spec.name}'
|
|
9070
10151
|
|
|
@@ -9081,6 +10162,24 @@ import {
|
|
|
9081
10162
|
${CONST_KEYWORD} name = parseApplicationName(' ${spec.name} ')
|
|
9082
10163
|
${CONST_KEYWORD} application = createApplication(name)
|
|
9083
10164
|
isApplicationError(new ApplicationError('CONFIG', 'invalid')) // true
|
|
10165
|
+
\`\`\``);
|
|
10166
|
+
if (hasBoundary) examples.push(`\`\`\`ts
|
|
10167
|
+
import type { ApplicationRecord } from '@app/core'
|
|
10168
|
+
import {
|
|
10169
|
+
APP_HEALTH_METHOD,
|
|
10170
|
+
APP_HEALTH_PATH,
|
|
10171
|
+
APP_HEALTH_TIMEOUT,
|
|
10172
|
+
isApplicationRecord,
|
|
10173
|
+
readApplicationHealth,
|
|
10174
|
+
} from '@app/core'
|
|
10175
|
+
|
|
10176
|
+
APP_HEALTH_METHOD // 'GET'
|
|
10177
|
+
APP_HEALTH_PATH // '/health'
|
|
10178
|
+
APP_HEALTH_TIMEOUT // 5000
|
|
10179
|
+
${CONST_KEYWORD} healthy: ApplicationRecord = { name: '${spec.name}', status: 'ok' }
|
|
10180
|
+
isApplicationRecord(healthy) // true
|
|
10181
|
+
isApplicationRecord({ name: '${spec.name}', status: 'down' }) // false
|
|
10182
|
+
await readApplicationHealth('http://127.0.0.1:3000') // { name: '${spec.name}' } or undefined
|
|
9084
10183
|
\`\`\``);
|
|
9085
10184
|
if (spec.app.includes("browser")) examples.push(`\`\`\`ts
|
|
9086
10185
|
import {
|
|
@@ -9096,27 +10195,36 @@ ${CONST_KEYWORD} browserOptions = parseBrowserApplicationOptions({
|
|
|
9096
10195
|
${CONST_KEYWORD} browser = createBrowserApplication(browserOptions)
|
|
9097
10196
|
browser.mount('#app')
|
|
9098
10197
|
isBrowserApplicationError(new BrowserApplicationError('CONFIG', 'invalid')) // true
|
|
10198
|
+
\`\`\``);
|
|
10199
|
+
if (hasShowcase) examples.push(`\`\`\`ts
|
|
10200
|
+
import { mountShowcaseApplication, seedApplication } from '@app/browser'
|
|
10201
|
+
|
|
10202
|
+
${CONST_KEYWORD} seed = seedApplication()
|
|
10203
|
+
${CONST_KEYWORD} showcase = mountShowcaseApplication('#app')
|
|
10204
|
+
seed.name // '${spec.name} showcase'
|
|
10205
|
+
showcase.unmount()
|
|
10206
|
+
\`\`\``);
|
|
10207
|
+
if (hasBoundary) examples.push(`\`\`\`ts
|
|
10208
|
+
import { mountBrowserApplication } from '@app/browser'
|
|
10209
|
+
|
|
10210
|
+
${CONST_KEYWORD} application = await mountBrowserApplication('#app')
|
|
10211
|
+
application.unmount()
|
|
9099
10212
|
\`\`\``);
|
|
9100
10213
|
if (spec.app.includes("server")) examples.push(`\`\`\`ts
|
|
9101
|
-
import {
|
|
9102
|
-
import {
|
|
10214
|
+
${hasBoundary ? `import type { ApplicationRecord } from '@app/core'
|
|
10215
|
+
import type { ApplicationState } from '@app/server'
|
|
10216
|
+
import { APP_HEALTH_METHOD, APP_HEALTH_PATH, isApplicationRecord } from '@app/core'` : "import type { ApplicationRecord, ApplicationState } from '@app/server'"}
|
|
9103
10217
|
import {
|
|
9104
|
-
APP_HEALTH_METHOD,
|
|
10218
|
+
${hasBoundary ? "" : ` APP_HEALTH_METHOD,
|
|
9105
10219
|
APP_HEALTH_PATH,
|
|
9106
|
-
|
|
9107
|
-
APP_HOST_LABEL_PATTERN,
|
|
9108
|
-
APP_KEEP_ALIVE_TIMEOUT,
|
|
9109
|
-
APP_MAX_CONNECTIONS,
|
|
9110
|
-
APP_MAX_HEADERS,
|
|
9111
|
-
APP_MAX_REQUESTS_PER_SOCKET,
|
|
9112
|
-
APP_REQUEST_TIMEOUT,
|
|
10220
|
+
`} APP_HOST_LABEL_PATTERN,
|
|
9113
10221
|
APP_NUMERIC_HOST_PATTERN,
|
|
9114
|
-
ApplicationServer,
|
|
9115
10222
|
ApplicationServerError,
|
|
9116
10223
|
DEFAULT_APP_START_TIMEOUT,
|
|
9117
10224
|
MAX_APP_START_TIMEOUT,
|
|
10225
|
+
createApplicationDispatcher,
|
|
9118
10226
|
createApplicationServer,
|
|
9119
|
-
|
|
10227
|
+
handleApplicationHealth,
|
|
9120
10228
|
isApplicationServerError,
|
|
9121
10229
|
parseApplicationHost,
|
|
9122
10230
|
parseApplicationPort,
|
|
@@ -9128,41 +10236,51 @@ import {
|
|
|
9128
10236
|
${CONST_KEYWORD} host = parseApplicationHost('127.0.0.1')
|
|
9129
10237
|
${CONST_KEYWORD} port = parseApplicationPort('0')
|
|
9130
10238
|
${CONST_KEYWORD} timeout = parseApplicationStartTimeout('5000')
|
|
9131
|
-
${CONST_KEYWORD} options = parseApplicationServerOptions({ host, port, timeout })
|
|
10239
|
+
${CONST_KEYWORD} options = parseApplicationServerOptions({ server: { host, port, timeout } })
|
|
10240
|
+
parseApplicationStartTimeout(String(DEFAULT_APP_START_TIMEOUT)) // 10000
|
|
10241
|
+
MAX_APP_START_TIMEOUT // 300000
|
|
9132
10242
|
APP_HOST_LABEL_PATTERN.test('api') // true
|
|
9133
10243
|
APP_NUMERIC_HOST_PATTERN.test('999.999.999.999') // true (and therefore rejected as a host)
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
${CONST_KEYWORD}
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
${CONST_KEYWORD}
|
|
9152
|
-
isApplicationServerError(
|
|
9153
|
-
reportApplicationServerError(
|
|
9154
|
-
|
|
10244
|
+
${CONST_KEYWORD} state: ApplicationState = { connection: { encrypted: false } }
|
|
10245
|
+
${CONST_KEYWORD} record: ApplicationRecord = { name: '${spec.name}', status: 'ok' }
|
|
10246
|
+
${CONST_KEYWORD} dispatcher = createApplicationDispatcher()
|
|
10247
|
+
try {
|
|
10248
|
+
${CONST_KEYWORD} response = await dispatcher.handle(
|
|
10249
|
+
new Request(\`http://application.test\${APP_HEALTH_PATH}\`, { method: APP_HEALTH_METHOD }),
|
|
10250
|
+
state,
|
|
10251
|
+
)
|
|
10252
|
+
${CONST_KEYWORD} health = handleApplicationHealth()
|
|
10253
|
+
${CONST_KEYWORD} encoded = Response.json(record)
|
|
10254
|
+
${hasBoundary ? `${CONST_KEYWORD} value: unknown = await health.clone().json()
|
|
10255
|
+
isApplicationRecord(value) // true
|
|
10256
|
+
` : ""}if (!response.ok || !health.ok || !encoded.ok) throw new Error('Application health failed')
|
|
10257
|
+
} finally {
|
|
10258
|
+
dispatcher.destroy()
|
|
10259
|
+
}
|
|
10260
|
+
|
|
10261
|
+
${CONST_KEYWORD} failure: unknown = new ApplicationServerError('CONFIG', 'invalid')
|
|
10262
|
+
if (isApplicationServerError(failure)) {
|
|
10263
|
+
reportApplicationServerError(failure) // writes only a stable CONFIG diagnostic
|
|
10264
|
+
}
|
|
9155
10265
|
|
|
9156
10266
|
${CONST_KEYWORD} server = createApplicationServer(options)
|
|
9157
10267
|
${CONST_KEYWORD} controller = new AbortController()
|
|
9158
10268
|
await server.start(controller.signal)
|
|
9159
10269
|
await server.stop()
|
|
10270
|
+
await server.destroy()
|
|
9160
10271
|
\`\`\`
|
|
9161
10272
|
|
|
9162
10273
|
\`\`\`ts
|
|
9163
|
-
import {
|
|
9164
|
-
|
|
9165
|
-
|
|
10274
|
+
import type { ApplicationServerRunnerEventMap, ApplicationServerRunnerOptions } from '@app/server'
|
|
10275
|
+
import { ApplicationServerRunner, createApplicationServer } from '@app/server'
|
|
10276
|
+
|
|
10277
|
+
${CONST_KEYWORD} event: keyof ApplicationServerRunnerEventMap = 'ready'
|
|
10278
|
+
${CONST_KEYWORD} observe: ApplicationServerRunnerOptions = { on: { fail: () => undefined } }
|
|
10279
|
+
${CONST_KEYWORD} runner = new ApplicationServerRunner(
|
|
10280
|
+
createApplicationServer({ server: { port: 0 } }),
|
|
10281
|
+
observe,
|
|
10282
|
+
)
|
|
10283
|
+
runner.emitter.once(event, (url) => console.log(url))
|
|
9166
10284
|
runner.start() // process owns shutdown signals
|
|
9167
10285
|
await runner.stop()
|
|
9168
10286
|
\`\`\`
|
|
@@ -9170,7 +10288,7 @@ await runner.stop()
|
|
|
9170
10288
|
\`\`\`ts
|
|
9171
10289
|
import { startApplicationServer } from '@app/server'
|
|
9172
10290
|
|
|
9173
|
-
${CONST_KEYWORD} processRunner = startApplicationServer({ port: 0 })
|
|
10291
|
+
${CONST_KEYWORD} processRunner = startApplicationServer({ server: { port: 0 } })
|
|
9174
10292
|
await processRunner.stop()
|
|
9175
10293
|
\`\`\``);
|
|
9176
10294
|
return examples.join("\n\n");
|
|
@@ -9193,15 +10311,23 @@ ${alignTable([
|
|
|
9193
10311
|
"Method",
|
|
9194
10312
|
"Returns",
|
|
9195
10313
|
"Behavior"
|
|
9196
|
-
], [[
|
|
9197
|
-
"`start`",
|
|
9198
|
-
"`Promise<void>`",
|
|
9199
|
-
"Serialize in call order; start only when stopped, and repeat safely when already listening. The optional `AbortSignal` and bounded startup timeout cancel pending name resolution/listen work. Rejects with `ApplicationServerError` code `LIFECYCLE` when startup fails, times out, or the caller aborts."
|
|
9200
10314
|
], [
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
10315
|
+
[
|
|
10316
|
+
"`start`",
|
|
10317
|
+
"`Promise<void>`",
|
|
10318
|
+
"Bind the installed `@orkestrel/server` substrate when idle or stopped. The optional `AbortSignal` and bounded startup timeout cancel pending binding. Rejects with `ApplicationServerError` code `LIFECYCLE` when startup fails, times out, or the caller aborts."
|
|
10319
|
+
],
|
|
10320
|
+
[
|
|
10321
|
+
"`stop`",
|
|
10322
|
+
"`Promise<void>`",
|
|
10323
|
+
"Drain and stop the installed server; repeated calls while stopped are safe. Rejects with `ApplicationServerError` code `LIFECYCLE` when closing fails."
|
|
10324
|
+
],
|
|
10325
|
+
[
|
|
10326
|
+
"`destroy`",
|
|
10327
|
+
"`Promise<void>`",
|
|
10328
|
+
"Perform terminal idempotent teardown through the installed server lifecycle, then destroy its owned dispatcher. Rejects with `ApplicationServerError` code `LIFECYCLE` when server teardown fails."
|
|
10329
|
+
]
|
|
10330
|
+
])}
|
|
9205
10331
|
|
|
9206
10332
|
#### \`ApplicationServerRunnerInterface\`
|
|
9207
10333
|
|
|
@@ -9212,17 +10338,18 @@ ${alignTable([
|
|
|
9212
10338
|
], [[
|
|
9213
10339
|
"`start`",
|
|
9214
10340
|
"`void`",
|
|
9215
|
-
"Register one
|
|
10341
|
+
"Register one generation-owned set of SIGINT/SIGTERM cleanup listeners, queue the substrate start behind any shutdown already in flight, emit `ready` after binding, and emit `fail` for a current lifecycle failure."
|
|
9216
10342
|
], [
|
|
9217
10343
|
"`stop`",
|
|
9218
10344
|
"`Promise<void>`",
|
|
9219
|
-
"
|
|
10345
|
+
"Abort a startup still in flight and release both process listeners, then wait for that startup to settle before stopping the server; concurrent calls join one substrate stop, and lifecycle failures emit `fail` and reject."
|
|
9220
10346
|
]])}
|
|
9221
10347
|
|
|
9222
|
-
The
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
\`
|
|
10348
|
+
The runner exposes its readonly \`emitter\`. \`ApplicationServerRunnerEventMap\` emits \`ready\` with the bound URL and \`fail\` with an \`unknown\` error. \`ApplicationServerRunnerOptions\` accepts initial \`on\` hooks and an emitter \`error\` handler; initial hooks run before the runner's own announcement and reporting listeners, so when no earlier failure set an exit code, a synchronous \`fail\` hook sees \`process.exitCode === undefined\` before the default reporter sets it to \`1\`. The default listeners preserve one exact \`[READY] <name> <url>\` stderr line and the stable redacted failure diagnostics. In-process consumers and tests park on runner events; a child process still observes the \`[READY]\` line because that byte stream is its process-boundary channel.
|
|
10349
|
+
|
|
10350
|
+
The application server constructor validates grouped direct options plus \`APP_HOST\`, \`APP_PORT\`, and
|
|
10351
|
+
\`APP_START_TIMEOUT\` before binding. Direct options must be an exact plain own-key data record containing only a
|
|
10352
|
+
\`server\` record with \`host\`, \`port\`, and/or \`timeout\`; inherited properties, accessors, symbols, instances, proxies that
|
|
9226
10353
|
throw during reflection, and unknown keys fail closed. Invalid values throw
|
|
9227
10354
|
\`ApplicationServerError\` code \`CONFIG\`; the default host is loopback and port \`0\` is
|
|
9228
10355
|
supported for collision-free ephemeral allocation. Startup defaults to 10 seconds and accepts
|
|
@@ -9230,10 +10357,15 @@ only integer timeouts from 1 through 300,000 milliseconds. Lifecycle failures us
|
|
|
9230
10357
|
\`LIFECYCLE\`; both may carry \`context.cause\` or \`context.value\`. Narrow caught values with
|
|
9231
10358
|
\`isApplicationServerError\` before reading either field.
|
|
9232
10359
|
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
10360
|
+
Before binding, \`url\` is \`undefined\`; after a successful start it reflects the real bound port,
|
|
10361
|
+
and it returns to \`undefined\` after stop or destroy. \`ApplicationState\` extends middleware's
|
|
10362
|
+
\`IdentifierState\` and adds only its \`connection\` property; there is no redundant \`listening\` member.
|
|
10363
|
+
|
|
10364
|
+
Each \`createApplicationDispatcher()\` call returns a fresh dispatcher that owns exactly \`GET /health\`
|
|
10365
|
+
and serializes the shared \`ApplicationRecord\` shape \`{ name: APP_NAME, status: 'ok' }\` as JSON. The
|
|
10366
|
+
server composes \`createBoundary()\`, \`createSecurity()\`, then \`createDeadline({ ms: timeout })\`
|
|
10367
|
+
around that owned dispatcher; standalone callers destroy theirs after use. Every other path returns
|
|
10368
|
+
\`404\`, and every unsupported method returns \`405\` with \`Allow: GET\`.`;
|
|
9237
10369
|
}
|
|
9238
10370
|
/**
|
|
9239
10371
|
* Build links to every generated source and application test file.
|
|
@@ -9243,7 +10375,7 @@ method returns deterministic plain-text \`405 Method Not Allowed\` with \`Allow:
|
|
|
9243
10375
|
* @returns A newline-separated Markdown test inventory.
|
|
9244
10376
|
*/
|
|
9245
10377
|
function guideTests(spec, pascal) {
|
|
9246
|
-
const tests = ["- [`tests/policy.test.ts`](../../tests/policy.test.ts) — filename placement and
|
|
10378
|
+
const tests = ["- [`tests/policy.test.ts`](../../tests/policy.test.ts) — repository coding law and filename placement.", "- [`tests/config/vite.test.ts`](../../tests/config/vite.test.ts) — executable root Vite invariants and conditional browser capability."];
|
|
9247
10379
|
for (const environment of spec.src) tests.push(`- [\`tests/src/${environment}/${pascal}.test.ts\`](../../tests/src/${environment}/${pascal}.test.ts) — entity boundaries.`, `- [\`tests/src/${environment}/factories.test.ts\`](../../tests/src/${environment}/factories.test.ts) — factory behavior.`);
|
|
9248
10380
|
if (spec.app.includes("core")) tests.push("- [`tests/app/core/factories.test.ts`](../../tests/app/core/factories.test.ts) — host-independent identity behavior.");
|
|
9249
10381
|
if (spec.app.includes("browser")) tests.push("- [`tests/app/browser/factories.test.ts`](../../tests/app/browser/factories.test.ts) — real-browser mount and cleanup.");
|
|
@@ -9396,12 +10528,15 @@ npm install @orkestrel/${blueprint.name}
|
|
|
9396
10528
|
\`\`\`` : "This is a private application workspace and is not published to npm.",
|
|
9397
10529
|
usage: guideUsage(blueprint, pascal)
|
|
9398
10530
|
}));
|
|
9399
|
-
if (selected.includes("orchestration"))
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
10531
|
+
if (selected.includes("orchestration")) {
|
|
10532
|
+
artifacts.push({
|
|
10533
|
+
path: ".github/workflows/ci.yml",
|
|
10534
|
+
group: "orchestration",
|
|
10535
|
+
origin: "computed",
|
|
10536
|
+
content: ciWorkflow(blueprint)
|
|
10537
|
+
});
|
|
10538
|
+
if (blueprint.services.length > 0) artifacts.push(fillArtifact(SERVICE_SCRIPT_PATH, "orchestration", "serviceProvisioner", {}));
|
|
10539
|
+
}
|
|
9405
10540
|
for (const path of selectHostPaths(HOST_PATHS, blueprint.name)) {
|
|
9406
10541
|
const group = hostGroup(path);
|
|
9407
10542
|
if (!selected.includes(group)) continue;
|
|
@@ -9525,9 +10660,10 @@ var Compiler = class Compiler {
|
|
|
9525
10660
|
this.#emitter.emit("audit", result);
|
|
9526
10661
|
return result;
|
|
9527
10662
|
}
|
|
10663
|
+
const diff = diffPlan(scaffolding.plan, current);
|
|
9528
10664
|
const result = {
|
|
9529
|
-
...
|
|
9530
|
-
questions: scaffolding.questions
|
|
10665
|
+
...diff,
|
|
10666
|
+
questions: [...scaffolding.questions, ...diff.questions]
|
|
9531
10667
|
};
|
|
9532
10668
|
this.#emitter.emit("audit", result);
|
|
9533
10669
|
return result;
|
|
@@ -9924,9 +11060,12 @@ function createBlueprint(data) {
|
|
|
9924
11060
|
}
|
|
9925
11061
|
//#endregion
|
|
9926
11062
|
exports.APP_BROWSER_DEV_DEPENDENCIES = APP_BROWSER_DEV_DEPENDENCIES;
|
|
11063
|
+
exports.APP_DEV_DEPENDENCIES = APP_DEV_DEPENDENCIES;
|
|
9927
11064
|
exports.APP_MATRIX = APP_MATRIX;
|
|
11065
|
+
exports.APP_SERVER_DEV_DEPENDENCIES = APP_SERVER_DEV_DEPENDENCIES;
|
|
9928
11066
|
exports.BASE_DEV_DEPENDENCIES = BASE_DEV_DEPENDENCIES;
|
|
9929
11067
|
exports.BIN_CONFIGS = BIN_CONFIGS;
|
|
11068
|
+
exports.CATALOG_AGENT_PATH = CATALOG_AGENT_PATH;
|
|
9930
11069
|
exports.CATEGORIES = CATEGORIES;
|
|
9931
11070
|
exports.CHECKOUT_ACTION_SHA = CHECKOUT_ACTION_SHA;
|
|
9932
11071
|
exports.COMPILER_ID = COMPILER_ID;
|
|
@@ -9966,6 +11105,8 @@ exports.MAX_SERIALIZED_INPUT_BYTES = MAX_SERIALIZED_INPUT_BYTES;
|
|
|
9966
11105
|
exports.MAX_TOTAL_ARTIFACT_BYTES = MAX_TOTAL_ARTIFACT_BYTES;
|
|
9967
11106
|
exports.MINIMUM_NODE_VERSION = MINIMUM_NODE_VERSION;
|
|
9968
11107
|
exports.NAME_PATTERN = NAME_PATTERN;
|
|
11108
|
+
exports.ORCHESTRATION_PATH_NAMES = ORCHESTRATION_PATH_NAMES;
|
|
11109
|
+
exports.ORCHESTRATION_PATH_PREFIXES = ORCHESTRATION_PATH_PREFIXES;
|
|
9969
11110
|
exports.ORIGINS = ORIGINS;
|
|
9970
11111
|
exports.ORKESTREL_RANGE_PATTERN = ORKESTREL_RANGE_PATTERN;
|
|
9971
11112
|
exports.PlanManager = PlanManager;
|
|
@@ -10003,6 +11144,7 @@ exports.compareCodeUnit = compareCodeUnit;
|
|
|
10003
11144
|
exports.computeColumnWidth = computeColumnWidth;
|
|
10004
11145
|
exports.computeHash = computeHash;
|
|
10005
11146
|
exports.configArtifacts = configArtifacts;
|
|
11147
|
+
exports.configViteProject = configViteProject;
|
|
10006
11148
|
exports.contentByteLength = contentByteLength;
|
|
10007
11149
|
exports.contentCodePoint = contentCodePoint;
|
|
10008
11150
|
exports.contentToBytes = contentToBytes;
|
|
@@ -10032,6 +11174,8 @@ exports.guideMethods = guideMethods;
|
|
|
10032
11174
|
exports.guideTests = guideTests;
|
|
10033
11175
|
exports.guideUsage = guideUsage;
|
|
10034
11176
|
exports.guidesViteProject = guidesViteProject;
|
|
11177
|
+
exports.hasApplicationBoundary = hasApplicationBoundary;
|
|
11178
|
+
exports.hasApplicationShowcase = hasApplicationShowcase;
|
|
10035
11179
|
exports.hasBlueprintEnvironment = hasBlueprintEnvironment;
|
|
10036
11180
|
exports.hasOnlyDataProperties = hasOnlyDataProperties;
|
|
10037
11181
|
exports.hasValidArtifactBytes = hasValidArtifactBytes;
|
|
@@ -10062,6 +11206,7 @@ exports.isSyncReport = isSyncReport;
|
|
|
10062
11206
|
exports.isWorkspaceName = isWorkspaceName;
|
|
10063
11207
|
exports.manifestToDependencies = manifestToDependencies;
|
|
10064
11208
|
exports.manifestToName = manifestToName;
|
|
11209
|
+
exports.matchesOrchestrationPath = matchesOrchestrationPath;
|
|
10065
11210
|
exports.member = member;
|
|
10066
11211
|
exports.memberShape = memberShape;
|
|
10067
11212
|
exports.override = override;
|
|
@@ -10087,6 +11232,7 @@ exports.policyViteProject = policyViteProject;
|
|
|
10087
11232
|
exports.rangeToFreshness = rangeToFreshness;
|
|
10088
11233
|
exports.renderArray = renderArray;
|
|
10089
11234
|
exports.renderObject = renderObject;
|
|
11235
|
+
exports.renderStringArray = renderStringArray;
|
|
10090
11236
|
exports.renderValue = renderValue;
|
|
10091
11237
|
exports.renderViteTest = renderViteTest;
|
|
10092
11238
|
exports.rootTsconfig = rootTsconfig;
|