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