@orkestrel/scaffold 0.0.19 → 0.0.21
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 +15 -9
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/CLAUDE.md +51 -24
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +69 -59
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +82 -0
- package/dist/host/claude/agents/application.md +34 -0
- package/dist/host/claude/agents/codex.md +7 -0
- package/dist/host/claude/agents/orkestrel.md +3 -3
- package/dist/host/claude/agents/researcher.md +31 -0
- package/dist/host/claude/agents/reviewer.md +5 -0
- package/dist/host/claude/agents/scout.md +25 -0
- package/dist/host/claude/rules/quality.md +10 -0
- package/dist/host/claude/rules/tests.md +10 -0
- package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +5 -1
- package/dist/host/codex/agents/application.toml +23 -0
- package/dist/host/codex/agents/researcher.toml +22 -0
- package/dist/host/codex/agents/reviewer.toml +3 -1
- package/dist/host/codex/agents/scout.toml +18 -0
- package/dist/host/codex/config.toml +15 -6
- package/dist/host/guides/src/scaffold.md +120 -29
- package/dist/host/manifest.json +35 -0
- package/dist/host/tests/setupPolicy.ts +49 -11
- package/dist/src/core/index.cjs +1661 -856
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +84 -7
- package/dist/src/core/index.d.ts +84 -7
- package/dist/src/core/index.js +1656 -857
- package/dist/src/core/index.js.map +1 -1
- package/package.json +4 -2
package/dist/src/core/index.js
CHANGED
|
@@ -228,15 +228,15 @@ var DEFAULT_VERSION = "0.0.1";
|
|
|
228
228
|
/** The `engines.node` range the `blueprint` builder fills. */
|
|
229
229
|
var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
|
|
230
230
|
/** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
|
|
231
|
-
var SCAFFOLD_RANGE = "^0.0.
|
|
231
|
+
var SCAFFOLD_RANGE = "^0.0.21";
|
|
232
232
|
/** Tooling versions shared by scaffold and every generated workspace. */
|
|
233
233
|
var BASE_DEV_DEPENDENCIES = Object.freeze({
|
|
234
234
|
"@microsoft/api-extractor": "^7.58.12",
|
|
235
235
|
"@orkestrel/guide": "^0.0.8",
|
|
236
236
|
"@orkestrel/scaffold": SCAFFOLD_RANGE,
|
|
237
237
|
"@types/node": "^26.1.2",
|
|
238
|
-
oxfmt: "^0.
|
|
239
|
-
oxlint: "^1.
|
|
238
|
+
oxfmt: "^0.62.0",
|
|
239
|
+
oxlint: "^1.77.0",
|
|
240
240
|
typescript: "^6.0.3",
|
|
241
241
|
vite: "~8.2.0",
|
|
242
242
|
"vite-plugin-dts": "^5.0.3",
|
|
@@ -247,13 +247,23 @@ var SOURCE_BROWSER_DEV_DEPENDENCIES = Object.freeze({
|
|
|
247
247
|
"@vitest/browser-playwright": "^4.1.10",
|
|
248
248
|
playwright: "^1.62.1"
|
|
249
249
|
});
|
|
250
|
+
/** Baseline development dependency required by every private application environment. */
|
|
251
|
+
var APP_DEV_DEPENDENCIES = Object.freeze({ "@orkestrel/contract": "^0.0.9" });
|
|
250
252
|
/** Additional development dependencies required by a private Vue browser application. */
|
|
251
253
|
var APP_BROWSER_DEV_DEPENDENCIES = Object.freeze({
|
|
252
254
|
...SOURCE_BROWSER_DEV_DEPENDENCIES,
|
|
255
|
+
"@orkestrel/html": "^0.0.2",
|
|
253
256
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
254
257
|
vue: "^3.5.40",
|
|
255
258
|
"vue-tsc": "^3.3.7"
|
|
256
259
|
});
|
|
260
|
+
/** Additional development dependencies required by a private server application. */
|
|
261
|
+
var APP_SERVER_DEV_DEPENDENCIES = Object.freeze({
|
|
262
|
+
"@orkestrel/emitter": "^0.0.5",
|
|
263
|
+
"@orkestrel/middleware": "^0.0.9",
|
|
264
|
+
"@orkestrel/router": "^0.0.8",
|
|
265
|
+
"@orkestrel/server": "^0.0.10"
|
|
266
|
+
});
|
|
257
267
|
/** Immutable official actions/checkout v6.0.2 commit used by generated CI. */
|
|
258
268
|
var CHECKOUT_ACTION_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
|
|
259
269
|
/** Immutable official actions/setup-node v6.4.0 commit used by generated CI. */
|
|
@@ -527,13 +537,45 @@ function serializeTypeScriptString(value) {
|
|
|
527
537
|
return `${output}'`;
|
|
528
538
|
}
|
|
529
539
|
/**
|
|
540
|
+
* Determine whether an application blueprint spans the shared browser/server boundary.
|
|
541
|
+
*
|
|
542
|
+
* @param spec - The blueprint to inspect.
|
|
543
|
+
* @returns True only when app/core, app/browser, and app/server are all selected.
|
|
544
|
+
*
|
|
545
|
+
* @example
|
|
546
|
+
* ```ts
|
|
547
|
+
* hasApplicationBoundary(blueprint('application', { app: ['core', 'browser', 'server'] }))
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
function hasApplicationBoundary(spec) {
|
|
551
|
+
return spec.app.includes("core") && spec.app.includes("browser") && spec.app.includes("server");
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Determine whether an application blueprint emits its browser showcase.
|
|
555
|
+
*
|
|
556
|
+
* @param spec - The blueprint to inspect.
|
|
557
|
+
* @returns True only when showcase intent accompanies app/browser.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* hasApplicationShowcase(blueprint('application', { app: ['browser'], showcase: true }))
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
564
|
+
function hasApplicationShowcase(spec) {
|
|
565
|
+
return spec.showcase && spec.app.includes("browser");
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
530
568
|
* Derive the declared public `Member[]` from a blueprint.
|
|
531
569
|
*
|
|
532
570
|
* @param spec - The blueprint to derive members from.
|
|
533
571
|
* @remarks
|
|
534
572
|
* Published source environments receive the canonical entity/type/factory/constant
|
|
535
573
|
* inventory. Application environments receive their exact public declaration kinds,
|
|
536
|
-
* including parsers, guards, handlers, errors, and runners where present.
|
|
574
|
+
* including parsers, guards, handlers, errors, and runners where present. Two groups
|
|
575
|
+
* move rather than duplicate: the health contract is declared against `app/server`
|
|
576
|
+
* while the server alone reads it and against `app/core` once the browser reads it
|
|
577
|
+
* too, and the showcase seed, factory, and root-view identity appear only for a
|
|
578
|
+
* blueprint whose showcase accompanies `app/browser`.
|
|
537
579
|
* @returns The declared `Member[]`, one set per environment.
|
|
538
580
|
*
|
|
539
581
|
* @example
|
|
@@ -546,6 +588,8 @@ function serializeTypeScriptString(value) {
|
|
|
546
588
|
function blueprintToMembers(spec) {
|
|
547
589
|
const pascal = pascalCase(spec.name);
|
|
548
590
|
const members = [];
|
|
591
|
+
const hasBoundary = hasApplicationBoundary(spec);
|
|
592
|
+
const hasShowcase = hasApplicationShowcase(spec);
|
|
549
593
|
for (const environment of spec.src) {
|
|
550
594
|
members.push(member(pascal, "entity", `The ${pascal} entity.`, environment));
|
|
551
595
|
members.push(member(`${pascal}Options`, "type", `Options for creating a ${pascal}.`, environment));
|
|
@@ -563,15 +607,21 @@ function blueprintToMembers(spec) {
|
|
|
563
607
|
members.push(member("isApplicationError", "guard", "Narrow a caught value to ApplicationError.", "core"));
|
|
564
608
|
members.push(member("parseApplicationName", "parser", "Parse an application name.", "core"));
|
|
565
609
|
members.push(member("createApplication", "factory", "Create an application identity.", "core"));
|
|
610
|
+
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
611
|
}
|
|
567
612
|
if (spec.app.includes("browser")) {
|
|
568
613
|
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
614
|
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
615
|
if (!spec.app.includes("core")) members.push(member("APP_NAME", "constant", "The browser application name.", "browser"));
|
|
616
|
+
if (hasShowcase && !spec.app.includes("core")) members.push(member("Application", "type", "The identity the root view renders.", "browser"));
|
|
617
|
+
if (hasShowcase) members.push(member("seedApplication", "factory", "Seed the inert showcase identity.", "browser"));
|
|
571
618
|
members.push(member("createBrowserApplication", "factory", "Create an unmounted Vue application.", "browser"));
|
|
619
|
+
if (hasShowcase) members.push(member("mountShowcaseApplication", "factory", "Mount the seeded showcase.", "browser"));
|
|
620
|
+
if (hasBoundary) members.push(member("mountBrowserApplication", "factory", "Mount the application over its server boundary.", "browser"));
|
|
572
621
|
}
|
|
573
622
|
if (spec.app.includes("server")) {
|
|
574
|
-
|
|
623
|
+
if (!hasBoundary) members.push(member("ApplicationRecord", "type", "The application health record.", "server"));
|
|
624
|
+
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
625
|
if (!spec.app.includes("core")) members.push(member("APP_NAME", "constant", "The server application name.", "server"));
|
|
576
626
|
}
|
|
577
627
|
return members;
|
|
@@ -1652,6 +1702,34 @@ function renderArray(entries, indent, prefix, suffix) {
|
|
|
1652
1702
|
return `[\n${items.map((item) => `${childIndent}${item}`).join(",\n")}\n${indent}]`;
|
|
1653
1703
|
}
|
|
1654
1704
|
/**
|
|
1705
|
+
* Render a single-quoted TypeScript string array literal through `oxfmt`'s
|
|
1706
|
+
* inline-or-broken rule — inline when the rendered width fits
|
|
1707
|
+
* `JSON_PRINT_WIDTH`, one item per line with a trailing comma on every line
|
|
1708
|
+
* (including the last) otherwise, matching `.oxfmtrc.json`'s
|
|
1709
|
+
* `trailingComma: "all"` for non-JSON files.
|
|
1710
|
+
*
|
|
1711
|
+
* @param entries - The array's string elements, in order.
|
|
1712
|
+
* @param indent - The current indentation prefix.
|
|
1713
|
+
* @param prefix - The text already emitted on this line before the array.
|
|
1714
|
+
* @param suffix - The text that will follow the array on this line.
|
|
1715
|
+
* @returns The rendered array fragment (no trailing newline).
|
|
1716
|
+
*
|
|
1717
|
+
* @example
|
|
1718
|
+
* ```ts
|
|
1719
|
+
* import { renderStringArray } from '@orkestrel/scaffold'
|
|
1720
|
+
*
|
|
1721
|
+
* renderStringArray(['app', 'guides', 'tests'], '', '', '') // "['app', 'guides', 'tests']"
|
|
1722
|
+
* ```
|
|
1723
|
+
*/
|
|
1724
|
+
function renderStringArray(entries, indent, prefix, suffix) {
|
|
1725
|
+
if (entries.length === 0) return "[]";
|
|
1726
|
+
const items = entries.map((entry) => serializeTypeScriptString(entry));
|
|
1727
|
+
const inline = `[${items.join(", ")}]`;
|
|
1728
|
+
if (fitsPrintWidth(`${prefix}${inline}${suffix}`)) return inline;
|
|
1729
|
+
const childIndent = `${indent}\t`;
|
|
1730
|
+
return `[\n${items.map((item) => `${childIndent}${item},`).join("\n")}\n${indent}]`;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1655
1733
|
* Render a JSON object through `formatJson`'s one-key-per-line rule.
|
|
1656
1734
|
*
|
|
1657
1735
|
* @param entry - The object to render.
|
|
@@ -2894,7 +2972,10 @@ export * from './factories.js'
|
|
|
2894
2972
|
name: "appCoreTypes",
|
|
2895
2973
|
summary: "The host-independent application contract.",
|
|
2896
2974
|
category: "source",
|
|
2897
|
-
placeholders: Object.freeze([
|
|
2975
|
+
placeholders: Object.freeze([Object.freeze({
|
|
2976
|
+
name: "record",
|
|
2977
|
+
description: "The shared health record declared once both hosts read it."
|
|
2978
|
+
})]),
|
|
2898
2979
|
content: `/** A rejected shared application boundary. */
|
|
2899
2980
|
${EXPORT_KEYWORD} type ApplicationErrorCode = 'CONFIG'
|
|
2900
2981
|
|
|
@@ -2908,7 +2989,7 @@ ${EXPORT_KEYWORD} interface ApplicationErrorContext {
|
|
|
2908
2989
|
${EXPORT_KEYWORD} interface Application {
|
|
2909
2990
|
readonly name: string
|
|
2910
2991
|
}
|
|
2911
|
-
`
|
|
2992
|
+
{{record}}`
|
|
2912
2993
|
}),
|
|
2913
2994
|
appCoreConstants: Object.freeze({
|
|
2914
2995
|
id: "appCoreConstants",
|
|
@@ -2918,6 +2999,9 @@ ${EXPORT_KEYWORD} interface Application {
|
|
|
2918
2999
|
placeholders: Object.freeze([Object.freeze({
|
|
2919
3000
|
name: "nameLiteral",
|
|
2920
3001
|
description: "The JSON-serialized application name."
|
|
3002
|
+
}), Object.freeze({
|
|
3003
|
+
name: "health",
|
|
3004
|
+
description: "The shared health route constants declared once both hosts read them."
|
|
2921
3005
|
})]),
|
|
2922
3006
|
content: `/** The application name shared by every host environment. */
|
|
2923
3007
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = {{nameLiteral}}
|
|
@@ -2927,7 +3011,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_LENGTH = 203
|
|
|
2927
3011
|
|
|
2928
3012
|
/** Maximum raw Unicode code units inspected before trimming an application name. */
|
|
2929
3013
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_INPUT_LENGTH = 255
|
|
2930
|
-
`
|
|
3014
|
+
{{health}}`
|
|
2931
3015
|
}),
|
|
2932
3016
|
appCoreErrors: Object.freeze({
|
|
2933
3017
|
id: "appCoreErrors",
|
|
@@ -2936,6 +3020,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APPLICATION_NAME_INPUT_LENGTH = 255
|
|
|
2936
3020
|
category: "source",
|
|
2937
3021
|
placeholders: Object.freeze([]),
|
|
2938
3022
|
content: `import type { ApplicationErrorCode, ApplicationErrorContext } from './types.js'
|
|
3023
|
+
import { holds } from '@orkestrel/contract'
|
|
2939
3024
|
|
|
2940
3025
|
/** A rejected shared application configuration value. */
|
|
2941
3026
|
${EXPORT_KEYWORD} class ApplicationError extends Error {
|
|
@@ -2964,11 +3049,34 @@ ${EXPORT_KEYWORD} class ApplicationError extends Error {
|
|
|
2964
3049
|
* \`\`\`
|
|
2965
3050
|
*/
|
|
2966
3051
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationError(value: unknown): value is ApplicationError {
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
3052
|
+
return holds(() => value instanceof ApplicationError)
|
|
3053
|
+
}
|
|
3054
|
+
`
|
|
3055
|
+
}),
|
|
3056
|
+
appCoreValidators: Object.freeze({
|
|
3057
|
+
id: "appCoreValidators",
|
|
3058
|
+
name: "appCoreValidators",
|
|
3059
|
+
summary: "The host-independent guard over the shared health record.",
|
|
3060
|
+
category: "source",
|
|
3061
|
+
placeholders: Object.freeze([]),
|
|
3062
|
+
content: `import type { ApplicationRecord } from './types.js'
|
|
3063
|
+
import { holds, isNonEmptyString, isRecord } from '@orkestrel/contract'
|
|
3064
|
+
|
|
3065
|
+
/**
|
|
3066
|
+
* Narrow one unvalidated transport value to the shared application record.
|
|
3067
|
+
*
|
|
3068
|
+
* @param value - The value read from the health route, before validation.
|
|
3069
|
+
* @returns True only for the exact record both hosts agreed on.
|
|
3070
|
+
*
|
|
3071
|
+
* @example
|
|
3072
|
+
* \`\`\`ts
|
|
3073
|
+
* import { isApplicationRecord } from '@app/core'
|
|
3074
|
+
*
|
|
3075
|
+
* isApplicationRecord({ name: 'example', status: 'ok' }) // true
|
|
3076
|
+
* \`\`\`
|
|
3077
|
+
*/
|
|
3078
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationRecord(value: unknown): value is ApplicationRecord {
|
|
3079
|
+
return holds(() => isRecord(value) && isNonEmptyString(value.name) && value.status === 'ok')
|
|
2972
3080
|
}
|
|
2973
3081
|
`
|
|
2974
3082
|
}),
|
|
@@ -2978,7 +3086,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationError(value: unknown): value
|
|
|
2978
3086
|
summary: "The host-independent application value parsers.",
|
|
2979
3087
|
category: "source",
|
|
2980
3088
|
placeholders: Object.freeze([]),
|
|
2981
|
-
content: `import {
|
|
3089
|
+
content: `import { isNonEmptyString } from '@orkestrel/contract'
|
|
3090
|
+
import { MAX_APPLICATION_NAME_INPUT_LENGTH, MAX_APPLICATION_NAME_LENGTH } from './constants.js'
|
|
2982
3091
|
import { ApplicationError } from './errors.js'
|
|
2983
3092
|
|
|
2984
3093
|
/**
|
|
@@ -3000,7 +3109,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationName(value: unknown): stri
|
|
|
3000
3109
|
throw new ApplicationError('CONFIG', 'Application name must be a string', { value })
|
|
3001
3110
|
}
|
|
3002
3111
|
const name = value.trim()
|
|
3003
|
-
if (name
|
|
3112
|
+
if (!isNonEmptyString(name) || name.length > MAX_APPLICATION_NAME_LENGTH) {
|
|
3004
3113
|
throw new ApplicationError(
|
|
3005
3114
|
'CONFIG',
|
|
3006
3115
|
\`Application name must contain 1 through \${MAX_APPLICATION_NAME_LENGTH} characters\`,
|
|
@@ -3009,6 +3118,52 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationName(value: unknown): stri
|
|
|
3009
3118
|
}
|
|
3010
3119
|
return name
|
|
3011
3120
|
}
|
|
3121
|
+
`
|
|
3122
|
+
}),
|
|
3123
|
+
appCoreHandlers: Object.freeze({
|
|
3124
|
+
id: "appCoreHandlers",
|
|
3125
|
+
name: "appCoreHandlers",
|
|
3126
|
+
summary: "The host-independent read of the shared health boundary.",
|
|
3127
|
+
category: "source",
|
|
3128
|
+
placeholders: Object.freeze([]),
|
|
3129
|
+
content: `import type { Application } from './types.js'
|
|
3130
|
+
import { APP_HEALTH_PATH, APP_HEALTH_TIMEOUT } from './constants.js'
|
|
3131
|
+
import { isApplicationRecord } from './validators.js'
|
|
3132
|
+
|
|
3133
|
+
/**
|
|
3134
|
+
* Read the application server's health route and translate its unvalidated JSON
|
|
3135
|
+
* into the shared application identity.
|
|
3136
|
+
*
|
|
3137
|
+
* @param origin - The absolute origin serving the application health route.
|
|
3138
|
+
* @returns The identity the running server reported, or undefined when the boundary
|
|
3139
|
+
* is unreachable, too slow, or off-contract.
|
|
3140
|
+
*
|
|
3141
|
+
* @remarks
|
|
3142
|
+
* The single translation point between the two hosts: the response body is read as
|
|
3143
|
+
* \`unknown\` and narrowed by {@link isApplicationRecord} before any field is consumed,
|
|
3144
|
+
* so a missing, slow, or foreign server degrades to \`undefined\` instead of leaking an
|
|
3145
|
+
* unvalidated value into the application. The record's \`status\` proves liveness; the
|
|
3146
|
+
* identity is what the caller renders.
|
|
3147
|
+
*
|
|
3148
|
+
* @example
|
|
3149
|
+
* \`\`\`ts
|
|
3150
|
+
* import { readApplicationHealth } from '@app/core'
|
|
3151
|
+
*
|
|
3152
|
+
* await readApplicationHealth('http://127.0.0.1:3000') // { name: 'example' }
|
|
3153
|
+
* \`\`\`
|
|
3154
|
+
*/
|
|
3155
|
+
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} readApplicationHealth(origin: string): Promise<Application | undefined> {
|
|
3156
|
+
try {
|
|
3157
|
+
const response = await fetch(new URL(APP_HEALTH_PATH, origin), {
|
|
3158
|
+
signal: AbortSignal.timeout(APP_HEALTH_TIMEOUT),
|
|
3159
|
+
})
|
|
3160
|
+
if (!response.ok) return undefined
|
|
3161
|
+
const record: unknown = await response.json()
|
|
3162
|
+
return isApplicationRecord(record) ? Object.freeze({ name: record.name }) : undefined
|
|
3163
|
+
} catch {
|
|
3164
|
+
return undefined
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3012
3167
|
`
|
|
3013
3168
|
}),
|
|
3014
3169
|
appCoreFactories: Object.freeze({
|
|
@@ -3044,12 +3199,18 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplication(name: string = APP_NAME)
|
|
|
3044
3199
|
name: "appCoreIndex",
|
|
3045
3200
|
summary: "The application core barrel.",
|
|
3046
3201
|
category: "source",
|
|
3047
|
-
placeholders: Object.freeze([
|
|
3202
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3203
|
+
name: "validators",
|
|
3204
|
+
description: "The optional shared-record guard barrel row."
|
|
3205
|
+
}), Object.freeze({
|
|
3206
|
+
name: "handlers",
|
|
3207
|
+
description: "The optional shared health-boundary barrel row."
|
|
3208
|
+
})]),
|
|
3048
3209
|
content: `export * from './types.js'
|
|
3049
3210
|
export * from './constants.js'
|
|
3050
3211
|
export * from './errors.js'
|
|
3051
|
-
export * from './parsers.js'
|
|
3052
|
-
export * from './factories.js'
|
|
3212
|
+
{{validators}}export * from './parsers.js'
|
|
3213
|
+
{{handlers}}export * from './factories.js'
|
|
3053
3214
|
`
|
|
3054
3215
|
}),
|
|
3055
3216
|
appBrowserTypes: Object.freeze({
|
|
@@ -3057,7 +3218,10 @@ export * from './factories.js'
|
|
|
3057
3218
|
name: "appBrowserTypes",
|
|
3058
3219
|
summary: "The browser application options.",
|
|
3059
3220
|
category: "source",
|
|
3060
|
-
placeholders: Object.freeze([
|
|
3221
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3222
|
+
name: "application",
|
|
3223
|
+
description: "The browser-owned root-view identity declared without application core."
|
|
3224
|
+
})]),
|
|
3061
3225
|
content: `/** A rejected browser application boundary. */
|
|
3062
3226
|
${EXPORT_KEYWORD} type BrowserApplicationErrorCode = 'CONFIG'
|
|
3063
3227
|
|
|
@@ -3071,7 +3235,7 @@ ${EXPORT_KEYWORD} interface BrowserApplicationErrorContext {
|
|
|
3071
3235
|
${EXPORT_KEYWORD} interface BrowserApplicationOptions {
|
|
3072
3236
|
readonly name?: string
|
|
3073
3237
|
}
|
|
3074
|
-
`
|
|
3238
|
+
{{application}}`
|
|
3075
3239
|
}),
|
|
3076
3240
|
appBrowserConstants: Object.freeze({
|
|
3077
3241
|
id: "appBrowserConstants",
|
|
@@ -3096,6 +3260,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH = 2
|
|
|
3096
3260
|
category: "source",
|
|
3097
3261
|
placeholders: Object.freeze([]),
|
|
3098
3262
|
content: `import type { BrowserApplicationErrorCode, BrowserApplicationErrorContext } from './types.js'
|
|
3263
|
+
import { holds } from '@orkestrel/contract'
|
|
3099
3264
|
|
|
3100
3265
|
/** A rejected browser application configuration value. */
|
|
3101
3266
|
${EXPORT_KEYWORD} class BrowserApplicationError extends Error {
|
|
@@ -3128,11 +3293,7 @@ ${EXPORT_KEYWORD} class BrowserApplicationError extends Error {
|
|
|
3128
3293
|
* \`\`\`
|
|
3129
3294
|
*/
|
|
3130
3295
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserApplicationError(value: unknown): value is BrowserApplicationError {
|
|
3131
|
-
|
|
3132
|
-
return value instanceof BrowserApplicationError
|
|
3133
|
-
} catch {
|
|
3134
|
-
return false
|
|
3135
|
-
}
|
|
3296
|
+
return holds(() => value instanceof BrowserApplicationError)
|
|
3136
3297
|
}
|
|
3137
3298
|
`
|
|
3138
3299
|
}),
|
|
@@ -3143,6 +3304,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserApplicationError(value: unknown):
|
|
|
3143
3304
|
category: "source",
|
|
3144
3305
|
placeholders: Object.freeze([]),
|
|
3145
3306
|
content: `import type { BrowserApplicationOptions } from './types.js'
|
|
3307
|
+
import { isNonEmptyString } from '@orkestrel/contract'
|
|
3146
3308
|
import {
|
|
3147
3309
|
MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH,
|
|
3148
3310
|
MAX_BROWSER_APPLICATION_NAME_LENGTH,
|
|
@@ -3179,6 +3341,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
|
|
|
3179
3341
|
{ value },
|
|
3180
3342
|
)
|
|
3181
3343
|
}
|
|
3344
|
+
// Walk own descriptors directly: reading values through a getter would run caller code.
|
|
3182
3345
|
const keys = Reflect.ownKeys(value)
|
|
3183
3346
|
if (keys.some((key) => key !== 'name')) {
|
|
3184
3347
|
throw new BrowserApplicationError('CONFIG', 'Unknown browser application option', {
|
|
@@ -3203,7 +3366,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
|
|
|
3203
3366
|
})
|
|
3204
3367
|
}
|
|
3205
3368
|
const name = descriptor.value.trim()
|
|
3206
|
-
if (name
|
|
3369
|
+
if (!isNonEmptyString(name) || name.length > MAX_BROWSER_APPLICATION_NAME_LENGTH) {
|
|
3207
3370
|
throw new BrowserApplicationError(
|
|
3208
3371
|
'CONFIG',
|
|
3209
3372
|
\`Browser application name must contain 1 through \${MAX_BROWSER_APPLICATION_NAME_LENGTH} characters\`,
|
|
@@ -3220,22 +3383,73 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseBrowserApplicationOptions(value: unkn
|
|
|
3220
3383
|
}
|
|
3221
3384
|
`
|
|
3222
3385
|
}),
|
|
3223
|
-
|
|
3224
|
-
id: "
|
|
3225
|
-
name: "
|
|
3226
|
-
summary: "The
|
|
3386
|
+
appBrowserSeeders: Object.freeze({
|
|
3387
|
+
id: "appBrowserSeeders",
|
|
3388
|
+
name: "appBrowserSeeders",
|
|
3389
|
+
summary: "The frozen, inert identity the showcase renders.",
|
|
3227
3390
|
category: "source",
|
|
3228
3391
|
placeholders: Object.freeze([Object.freeze({
|
|
3392
|
+
name: "applicationImport",
|
|
3393
|
+
description: "The selected layer type import for the root-view identity."
|
|
3394
|
+
}), Object.freeze({
|
|
3229
3395
|
name: "nameImport",
|
|
3230
3396
|
description: "The selected layer import for APP_NAME."
|
|
3231
3397
|
})]),
|
|
3398
|
+
content: `{{applicationImport}}
|
|
3399
|
+
{{nameImport}}
|
|
3400
|
+
|
|
3401
|
+
/**
|
|
3402
|
+
* Seed the inert identity the showcase renders.
|
|
3403
|
+
*
|
|
3404
|
+
* @returns A fresh frozen identity, identical on every call.
|
|
3405
|
+
*
|
|
3406
|
+
* @remarks
|
|
3407
|
+
* The showcase's only data. It is exactly the value the shipped root view receives from
|
|
3408
|
+
* the running application, so the showcase exercises the shipped view rather than a
|
|
3409
|
+
* parallel copy of it.
|
|
3410
|
+
*
|
|
3411
|
+
* @example
|
|
3412
|
+
* \`\`\`ts
|
|
3413
|
+
* import { seedApplication } from '@app/browser'
|
|
3414
|
+
*
|
|
3415
|
+
* seedApplication().name // the seeded showcase identity
|
|
3416
|
+
* \`\`\`
|
|
3417
|
+
*/
|
|
3418
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} seedApplication(): Application {
|
|
3419
|
+
return Object.freeze({ name: \`\${APP_NAME} showcase\` })
|
|
3420
|
+
}
|
|
3421
|
+
`
|
|
3422
|
+
}),
|
|
3423
|
+
appBrowserFactories: Object.freeze({
|
|
3424
|
+
id: "appBrowserFactories",
|
|
3425
|
+
name: "appBrowserFactories",
|
|
3426
|
+
summary: "The Vue browser application factory.",
|
|
3427
|
+
category: "source",
|
|
3428
|
+
placeholders: Object.freeze([
|
|
3429
|
+
Object.freeze({
|
|
3430
|
+
name: "nameImport",
|
|
3431
|
+
description: "The selected layer import for APP_NAME."
|
|
3432
|
+
}),
|
|
3433
|
+
Object.freeze({
|
|
3434
|
+
name: "seedImport",
|
|
3435
|
+
description: "The optional showcase seeder import."
|
|
3436
|
+
}),
|
|
3437
|
+
Object.freeze({
|
|
3438
|
+
name: "showcase",
|
|
3439
|
+
description: "The optional seeded showcase factory."
|
|
3440
|
+
}),
|
|
3441
|
+
Object.freeze({
|
|
3442
|
+
name: "boundary",
|
|
3443
|
+
description: "The optional server-boundary startup factory."
|
|
3444
|
+
})
|
|
3445
|
+
]),
|
|
3232
3446
|
content: `import type { App } from 'vue'
|
|
3233
3447
|
import type { BrowserApplicationOptions } from './types.js'
|
|
3234
3448
|
import { createApp } from 'vue'
|
|
3235
3449
|
import ApplicationView from './ApplicationView.vue'
|
|
3236
3450
|
{{nameImport}}
|
|
3237
3451
|
import { parseBrowserApplicationOptions } from './parsers.js'
|
|
3238
|
-
|
|
3452
|
+
{{seedImport}}
|
|
3239
3453
|
/**
|
|
3240
3454
|
* Create an unmounted Vue application.
|
|
3241
3455
|
*
|
|
@@ -3253,19 +3467,22 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createBrowserApplication(options: BrowserA
|
|
|
3253
3467
|
const parsed = parseBrowserApplicationOptions(options)
|
|
3254
3468
|
return createApp(ApplicationView, { name: parsed.name ?? APP_NAME })
|
|
3255
3469
|
}
|
|
3256
|
-
`
|
|
3470
|
+
{{showcase}}{{boundary}}`
|
|
3257
3471
|
}),
|
|
3258
3472
|
appBrowserIndex: Object.freeze({
|
|
3259
3473
|
id: "appBrowserIndex",
|
|
3260
3474
|
name: "appBrowserIndex",
|
|
3261
3475
|
summary: "The browser application barrel.",
|
|
3262
3476
|
category: "source",
|
|
3263
|
-
placeholders: Object.freeze([
|
|
3477
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3478
|
+
name: "seeders",
|
|
3479
|
+
description: "The optional showcase seeder barrel row."
|
|
3480
|
+
})]),
|
|
3264
3481
|
content: `export * from './types.js'
|
|
3265
3482
|
export * from './constants.js'
|
|
3266
3483
|
export * from './errors.js'
|
|
3267
3484
|
export * from './parsers.js'
|
|
3268
|
-
export * from './factories.js'
|
|
3485
|
+
{{seeders}}export * from './factories.js'
|
|
3269
3486
|
`
|
|
3270
3487
|
}),
|
|
3271
3488
|
appBrowserMain: Object.freeze({
|
|
@@ -3273,10 +3490,27 @@ export * from './factories.js'
|
|
|
3273
3490
|
name: "appBrowserMain",
|
|
3274
3491
|
summary: "The browser executable entry.",
|
|
3275
3492
|
category: "source",
|
|
3493
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3494
|
+
name: "factory",
|
|
3495
|
+
description: "The factory the shipped entry mounts through."
|
|
3496
|
+
}), Object.freeze({
|
|
3497
|
+
name: "mount",
|
|
3498
|
+
description: "The mounting statement."
|
|
3499
|
+
})]),
|
|
3500
|
+
content: `import { {{factory}} } from './index.js'
|
|
3501
|
+
|
|
3502
|
+
{{mount}}
|
|
3503
|
+
`
|
|
3504
|
+
}),
|
|
3505
|
+
appBrowserShowcase: Object.freeze({
|
|
3506
|
+
id: "appBrowserShowcase",
|
|
3507
|
+
name: "appBrowserShowcase",
|
|
3508
|
+
summary: "The showcase executable entry.",
|
|
3509
|
+
category: "source",
|
|
3276
3510
|
placeholders: Object.freeze([]),
|
|
3277
|
-
content: `import {
|
|
3511
|
+
content: `import { mountShowcaseApplication } from './index.js'
|
|
3278
3512
|
|
|
3279
|
-
|
|
3513
|
+
mountShowcaseApplication('#app')
|
|
3280
3514
|
`
|
|
3281
3515
|
}),
|
|
3282
3516
|
appBrowserView: Object.freeze({
|
|
@@ -3321,6 +3555,33 @@ defineProps<{ readonly name: string }>()
|
|
|
3321
3555
|
<script type="module" src="/main.ts"><\/script>
|
|
3322
3556
|
</body>
|
|
3323
3557
|
</html>
|
|
3558
|
+
`
|
|
3559
|
+
}),
|
|
3560
|
+
appBrowserShowcaseHtml: Object.freeze({
|
|
3561
|
+
id: "appBrowserShowcaseHtml",
|
|
3562
|
+
name: "appBrowserShowcaseHtml",
|
|
3563
|
+
summary: "The showcase HTML entry with its development security policy.",
|
|
3564
|
+
category: "source",
|
|
3565
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3566
|
+
name: "name",
|
|
3567
|
+
description: "The application name."
|
|
3568
|
+
})]),
|
|
3569
|
+
content: `<!doctype html>
|
|
3570
|
+
<html lang="en">
|
|
3571
|
+
<head>
|
|
3572
|
+
<meta
|
|
3573
|
+
http-equiv="Content-Security-Policy"
|
|
3574
|
+
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'"
|
|
3575
|
+
/>
|
|
3576
|
+
<meta charset="UTF-8" />
|
|
3577
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
3578
|
+
<title>{{name}} showcase</title>
|
|
3579
|
+
</head>
|
|
3580
|
+
<body>
|
|
3581
|
+
<div id="app"></div>
|
|
3582
|
+
<script type="module" src="/showcase.ts"><\/script>
|
|
3583
|
+
</body>
|
|
3584
|
+
</html>
|
|
3324
3585
|
`
|
|
3325
3586
|
}),
|
|
3326
3587
|
appBrowserEnv: Object.freeze({
|
|
@@ -3344,8 +3605,20 @@ declare module '*.vue' {
|
|
|
3344
3605
|
name: "appServerTypes",
|
|
3345
3606
|
summary: "The application server contract.",
|
|
3346
3607
|
category: "source",
|
|
3347
|
-
placeholders: Object.freeze([
|
|
3348
|
-
|
|
3608
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3609
|
+
name: "record",
|
|
3610
|
+
description: "The health record, declared here only while the server alone reads it."
|
|
3611
|
+
})]),
|
|
3612
|
+
content: `import type { EmitterErrorHandler, EmitterHooks, EmitterInterface } from '@orkestrel/emitter'
|
|
3613
|
+
import type { IdentifierState } from '@orkestrel/middleware'
|
|
3614
|
+
import type { ConnectionInfo, ServerStatus } from '@orkestrel/server'
|
|
3615
|
+
|
|
3616
|
+
{{record}}/** Per-request application state derived from connection facts. */
|
|
3617
|
+
${EXPORT_KEYWORD} interface ApplicationState extends IdentifierState {
|
|
3618
|
+
readonly connection: ConnectionInfo
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
/** A rejected application server boundary. */
|
|
3349
3622
|
${EXPORT_KEYWORD} type ApplicationServerErrorCode = 'CONFIG' | 'LIFECYCLE'
|
|
3350
3623
|
|
|
3351
3624
|
/** Diagnostic context attached to an application server boundary error. */
|
|
@@ -3356,26 +3629,49 @@ ${EXPORT_KEYWORD} interface ApplicationServerErrorContext {
|
|
|
3356
3629
|
|
|
3357
3630
|
/** Options for creating an application server. */
|
|
3358
3631
|
${EXPORT_KEYWORD} interface ApplicationServerOptions {
|
|
3359
|
-
readonly
|
|
3360
|
-
|
|
3361
|
-
|
|
3632
|
+
readonly server?: {
|
|
3633
|
+
readonly host?: string
|
|
3634
|
+
readonly port?: number
|
|
3635
|
+
readonly timeout?: number
|
|
3636
|
+
}
|
|
3362
3637
|
}
|
|
3363
3638
|
|
|
3364
3639
|
/** A lifecycle-safe application server. */
|
|
3365
3640
|
${EXPORT_KEYWORD} interface ApplicationServerInterface {
|
|
3366
3641
|
readonly host: string
|
|
3367
|
-
readonly port: number
|
|
3368
|
-
readonly
|
|
3369
|
-
readonly url: string
|
|
3642
|
+
readonly port: number | undefined
|
|
3643
|
+
readonly status: ServerStatus
|
|
3644
|
+
readonly url: string | undefined
|
|
3370
3645
|
start(signal?: AbortSignal): Promise<void>
|
|
3371
3646
|
stop(): Promise<void>
|
|
3647
|
+
destroy(): Promise<void>
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
/** Observable process-lifecycle outcomes for an application server runner. */
|
|
3651
|
+
${EXPORT_KEYWORD} type ApplicationServerRunnerEventMap = {
|
|
3652
|
+
readonly ready: readonly [url: string]
|
|
3653
|
+
readonly fail: readonly [error: unknown]
|
|
3372
3654
|
}
|
|
3373
3655
|
|
|
3374
3656
|
/** The process lifecycle owner for an application server. */
|
|
3375
3657
|
${EXPORT_KEYWORD} interface ApplicationServerRunnerInterface {
|
|
3658
|
+
readonly emitter: EmitterInterface<ApplicationServerRunnerEventMap>
|
|
3376
3659
|
start(): void
|
|
3377
3660
|
stop(): Promise<void>
|
|
3378
3661
|
}
|
|
3662
|
+
|
|
3663
|
+
/**
|
|
3664
|
+
* Options for observing an application server runner.
|
|
3665
|
+
*
|
|
3666
|
+
* @remarks
|
|
3667
|
+
* Initial \`on\` hooks run before the runner's own readiness and failure effects. When no earlier
|
|
3668
|
+
* failure set an exit code, a synchronous fail hook therefore observes \`process.exitCode\` as
|
|
3669
|
+
* \`undefined\` before the default reporter sets it to \`1\`.
|
|
3670
|
+
*/
|
|
3671
|
+
${EXPORT_KEYWORD} interface ApplicationServerRunnerOptions {
|
|
3672
|
+
readonly on?: EmitterHooks<ApplicationServerRunnerEventMap>
|
|
3673
|
+
readonly error?: EmitterErrorHandler
|
|
3674
|
+
}
|
|
3379
3675
|
`
|
|
3380
3676
|
}),
|
|
3381
3677
|
appServerConstants: Object.freeze({
|
|
@@ -3386,6 +3682,9 @@ ${EXPORT_KEYWORD} interface ApplicationServerRunnerInterface {
|
|
|
3386
3682
|
placeholders: Object.freeze([Object.freeze({
|
|
3387
3683
|
name: "nameConstant",
|
|
3388
3684
|
description: "The optional server-only APP_NAME declaration."
|
|
3685
|
+
}), Object.freeze({
|
|
3686
|
+
name: "health",
|
|
3687
|
+
description: "The health route constants, declared here only while the server alone reads them."
|
|
3389
3688
|
})]),
|
|
3390
3689
|
content: `{{nameConstant}}/** The fail-closed loopback host default. */
|
|
3391
3690
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} DEFAULT_APP_HOST = '127.0.0.1'
|
|
@@ -3405,24 +3704,6 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APP_HOST_INPUT_LENGTH = 255
|
|
|
3405
3704
|
/** Maximum raw characters inspected at an application numeric boundary. */
|
|
3406
3705
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} MAX_APP_NUMBER_INPUT_LENGTH = 32
|
|
3407
3706
|
|
|
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
3707
|
/** The decimal-only syntax accepted at the APP_PORT string boundary. */
|
|
3427
3708
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_PORT_PATTERN = /^\\d+$/
|
|
3428
3709
|
|
|
@@ -3431,13 +3712,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HOST_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Z
|
|
|
3431
3712
|
|
|
3432
3713
|
/** Numeric-looking non-IP hosts rejected before platform DNS interpretation. */
|
|
3433
3714
|
${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
|
-
`
|
|
3715
|
+
{{health}}`
|
|
3441
3716
|
}),
|
|
3442
3717
|
appServerErrors: Object.freeze({
|
|
3443
3718
|
id: "appServerErrors",
|
|
@@ -3446,6 +3721,7 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/'
|
|
|
3446
3721
|
category: "source",
|
|
3447
3722
|
placeholders: Object.freeze([]),
|
|
3448
3723
|
content: `import type { ApplicationServerErrorCode, ApplicationServerErrorContext } from './types.js'
|
|
3724
|
+
import { holds } from '@orkestrel/contract'
|
|
3449
3725
|
|
|
3450
3726
|
/** A rejected application server configuration or lifecycle operation. */
|
|
3451
3727
|
${EXPORT_KEYWORD} class ApplicationServerError extends Error {
|
|
@@ -3478,11 +3754,7 @@ ${EXPORT_KEYWORD} class ApplicationServerError extends Error {
|
|
|
3478
3754
|
* \`\`\`
|
|
3479
3755
|
*/
|
|
3480
3756
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationServerError(value: unknown): value is ApplicationServerError {
|
|
3481
|
-
|
|
3482
|
-
return value instanceof ApplicationServerError
|
|
3483
|
-
} catch {
|
|
3484
|
-
return false
|
|
3485
|
-
}
|
|
3757
|
+
return holds(() => value instanceof ApplicationServerError)
|
|
3486
3758
|
}
|
|
3487
3759
|
`
|
|
3488
3760
|
}),
|
|
@@ -3493,6 +3765,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isApplicationServerError(value: unknown):
|
|
|
3493
3765
|
category: "source",
|
|
3494
3766
|
placeholders: Object.freeze([]),
|
|
3495
3767
|
content: `import type { ApplicationServerOptions } from './types.js'
|
|
3768
|
+
import { isNonEmptyString, parseString } from '@orkestrel/contract'
|
|
3496
3769
|
import { isIP } from 'node:net'
|
|
3497
3770
|
import {
|
|
3498
3771
|
APP_HOST_LABEL_PATTERN,
|
|
@@ -3522,7 +3795,7 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationHost(value: unknown): stri
|
|
|
3522
3795
|
const host = value.trim()
|
|
3523
3796
|
const family = isIP(host)
|
|
3524
3797
|
if (
|
|
3525
|
-
host
|
|
3798
|
+
!isNonEmptyString(host) ||
|
|
3526
3799
|
host.length > 253 ||
|
|
3527
3800
|
(family === 0 &&
|
|
3528
3801
|
(APP_NUMERIC_HOST_PATTERN.test(host) ||
|
|
@@ -3544,18 +3817,14 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationHost(value: unknown): stri
|
|
|
3544
3817
|
*/
|
|
3545
3818
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationPort(value: unknown): number {
|
|
3546
3819
|
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) {
|
|
3820
|
+
const text = parseString(value)
|
|
3821
|
+
if (text === undefined || text.length > MAX_APP_NUMBER_INPUT_LENGTH) {
|
|
3553
3822
|
throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
|
|
3554
3823
|
value,
|
|
3555
3824
|
})
|
|
3556
3825
|
}
|
|
3557
|
-
const
|
|
3558
|
-
const port = APP_PORT_PATTERN.test(
|
|
3826
|
+
const normalized = text.trim()
|
|
3827
|
+
const port = APP_PORT_PATTERN.test(normalized) ? Number(normalized) : Number.NaN
|
|
3559
3828
|
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
3560
3829
|
throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
|
|
3561
3830
|
value,
|
|
@@ -3573,22 +3842,16 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationPort(value: unknown): numb
|
|
|
3573
3842
|
*/
|
|
3574
3843
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationStartTimeout(value: unknown): number {
|
|
3575
3844
|
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) {
|
|
3845
|
+
const text = parseString(value)
|
|
3846
|
+
if (text === undefined || text.length > MAX_APP_NUMBER_INPUT_LENGTH) {
|
|
3584
3847
|
throw new ApplicationServerError(
|
|
3585
3848
|
'CONFIG',
|
|
3586
3849
|
\`APP_START_TIMEOUT must be an integer from 1 through \${MAX_APP_START_TIMEOUT}\`,
|
|
3587
3850
|
{ value },
|
|
3588
3851
|
)
|
|
3589
3852
|
}
|
|
3590
|
-
const
|
|
3591
|
-
const timeout = APP_PORT_PATTERN.test(
|
|
3853
|
+
const normalized = text.trim()
|
|
3854
|
+
const timeout = APP_PORT_PATTERN.test(normalized) ? Number(normalized) : Number.NaN
|
|
3592
3855
|
if (!Number.isInteger(timeout) || timeout < 1 || timeout > MAX_APP_START_TIMEOUT) {
|
|
3593
3856
|
throw new ApplicationServerError(
|
|
3594
3857
|
'CONFIG',
|
|
@@ -3624,24 +3887,58 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
|
|
|
3624
3887
|
)
|
|
3625
3888
|
}
|
|
3626
3889
|
const keys = Reflect.ownKeys(value)
|
|
3627
|
-
const unknown = keys.filter((key) => key !== '
|
|
3890
|
+
const unknown = keys.filter((key) => key !== 'server')
|
|
3628
3891
|
if (unknown.length > 0) {
|
|
3629
3892
|
throw new ApplicationServerError('CONFIG', 'Unknown application server option', { value })
|
|
3630
3893
|
}
|
|
3631
|
-
const
|
|
3632
|
-
|
|
3633
|
-
|
|
3894
|
+
const serverDescriptor = Reflect.getOwnPropertyDescriptor(value, 'server')
|
|
3895
|
+
if (
|
|
3896
|
+
keys.includes('server') &&
|
|
3897
|
+
(serverDescriptor === undefined || !Reflect.has(serverDescriptor, 'value'))
|
|
3898
|
+
) {
|
|
3899
|
+
throw new ApplicationServerError(
|
|
3900
|
+
'CONFIG',
|
|
3901
|
+
'Application server options must use data properties',
|
|
3902
|
+
{ value },
|
|
3903
|
+
)
|
|
3904
|
+
}
|
|
3905
|
+
const server = serverDescriptor?.value
|
|
3906
|
+
if (server === undefined) return {}
|
|
3907
|
+
if (typeof server !== 'object' || server === null || Array.isArray(server)) {
|
|
3908
|
+
throw new ApplicationServerError('CONFIG', 'Application server settings must be an object', {
|
|
3909
|
+
value,
|
|
3910
|
+
})
|
|
3911
|
+
}
|
|
3912
|
+
const serverPrototype = Reflect.getPrototypeOf(server)
|
|
3913
|
+
if (serverPrototype !== Object.prototype && serverPrototype !== null) {
|
|
3914
|
+
throw new ApplicationServerError(
|
|
3915
|
+
'CONFIG',
|
|
3916
|
+
'Application server settings must be a plain record',
|
|
3917
|
+
{ value },
|
|
3918
|
+
)
|
|
3919
|
+
}
|
|
3920
|
+
// Walk own descriptors directly: reading values through a getter would run caller code.
|
|
3921
|
+
const serverKeys = Reflect.ownKeys(server)
|
|
3922
|
+
const serverUnknown = serverKeys.filter(
|
|
3923
|
+
(key) => key !== 'host' && key !== 'port' && key !== 'timeout',
|
|
3924
|
+
)
|
|
3925
|
+
if (serverUnknown.length > 0) {
|
|
3926
|
+
throw new ApplicationServerError('CONFIG', 'Unknown application server setting', { value })
|
|
3927
|
+
}
|
|
3928
|
+
const hostDescriptor = Reflect.getOwnPropertyDescriptor(server, 'host')
|
|
3929
|
+
const portDescriptor = Reflect.getOwnPropertyDescriptor(server, 'port')
|
|
3930
|
+
const timeoutDescriptor = Reflect.getOwnPropertyDescriptor(server, 'timeout')
|
|
3634
3931
|
if (
|
|
3635
|
-
(
|
|
3932
|
+
(serverKeys.includes('host') &&
|
|
3636
3933
|
(hostDescriptor === undefined || !Reflect.has(hostDescriptor, 'value'))) ||
|
|
3637
|
-
(
|
|
3934
|
+
(serverKeys.includes('port') &&
|
|
3638
3935
|
(portDescriptor === undefined || !Reflect.has(portDescriptor, 'value'))) ||
|
|
3639
|
-
(
|
|
3936
|
+
(serverKeys.includes('timeout') &&
|
|
3640
3937
|
(timeoutDescriptor === undefined || !Reflect.has(timeoutDescriptor, 'value')))
|
|
3641
3938
|
) {
|
|
3642
3939
|
throw new ApplicationServerError(
|
|
3643
3940
|
'CONFIG',
|
|
3644
|
-
'Application server
|
|
3941
|
+
'Application server settings must use data properties',
|
|
3645
3942
|
{ value },
|
|
3646
3943
|
)
|
|
3647
3944
|
}
|
|
@@ -3649,9 +3946,11 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
|
|
|
3649
3946
|
const port = portDescriptor?.value
|
|
3650
3947
|
const timeout = timeoutDescriptor?.value
|
|
3651
3948
|
return {
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3949
|
+
server: {
|
|
3950
|
+
...(host === undefined ? {} : { host: parseApplicationHost(host) }),
|
|
3951
|
+
...(port === undefined ? {} : { port: parseApplicationPort(port) }),
|
|
3952
|
+
...(timeout === undefined ? {} : { timeout: parseApplicationStartTimeout(timeout) }),
|
|
3953
|
+
},
|
|
3655
3954
|
}
|
|
3656
3955
|
} catch (error) {
|
|
3657
3956
|
if (isApplicationServerError(error)) throw error
|
|
@@ -3660,47 +3959,56 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} parseApplicationServerOptions(value: unkno
|
|
|
3660
3959
|
})
|
|
3661
3960
|
}
|
|
3662
3961
|
}
|
|
3962
|
+
`
|
|
3963
|
+
}),
|
|
3964
|
+
appServerRoutes: Object.freeze({
|
|
3965
|
+
id: "appServerRoutes",
|
|
3966
|
+
name: "appServerRoutes",
|
|
3967
|
+
summary: "The standalone application route dispatcher factory.",
|
|
3968
|
+
category: "source",
|
|
3969
|
+
placeholders: Object.freeze([Object.freeze({
|
|
3970
|
+
name: "healthImport",
|
|
3971
|
+
description: "The selected layer import for the health route constants."
|
|
3972
|
+
})]),
|
|
3973
|
+
content: `import type { DispatcherInterface } from '@orkestrel/router'
|
|
3974
|
+
import type { ApplicationState } from './types.js'
|
|
3975
|
+
import { createDispatcher } from '@orkestrel/router'
|
|
3976
|
+
{{healthImport}}
|
|
3977
|
+
import { handleApplicationHealth } from './handlers.js'
|
|
3978
|
+
|
|
3979
|
+
/** Create a fresh dispatcher for the generated application's fetch-standard health boundary. */
|
|
3980
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationDispatcher(): DispatcherInterface<ApplicationState> {
|
|
3981
|
+
return createDispatcher<ApplicationState>({
|
|
3982
|
+
routes: [
|
|
3983
|
+
{
|
|
3984
|
+
method: APP_HEALTH_METHOD,
|
|
3985
|
+
path: APP_HEALTH_PATH,
|
|
3986
|
+
handler: handleApplicationHealth,
|
|
3987
|
+
},
|
|
3988
|
+
],
|
|
3989
|
+
})
|
|
3990
|
+
}
|
|
3663
3991
|
`
|
|
3664
3992
|
}),
|
|
3665
3993
|
appServerHandlers: Object.freeze({
|
|
3666
3994
|
id: "appServerHandlers",
|
|
3667
3995
|
name: "appServerHandlers",
|
|
3668
|
-
summary: "The server
|
|
3996
|
+
summary: "The application server diagnostic reporter.",
|
|
3669
3997
|
category: "source",
|
|
3670
3998
|
placeholders: Object.freeze([Object.freeze({
|
|
3999
|
+
name: "recordImport",
|
|
4000
|
+
description: "The selected layer type import for the health record."
|
|
4001
|
+
}), Object.freeze({
|
|
3671
4002
|
name: "nameImport",
|
|
3672
4003
|
description: "The selected layer import for APP_NAME."
|
|
3673
4004
|
})]),
|
|
3674
|
-
content: `
|
|
4005
|
+
content: `{{recordImport}}
|
|
3675
4006
|
{{nameImport}}
|
|
3676
|
-
import { APP_HEALTH_METHOD, APP_HEALTH_PATH } from './constants.js'
|
|
3677
4007
|
import { isApplicationServerError } from './errors.js'
|
|
3678
4008
|
|
|
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' }))
|
|
4009
|
+
/** Return the generated application's shared health record. */
|
|
4010
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} handleApplicationHealth(): Response {
|
|
4011
|
+
return Response.json({ name: APP_NAME, status: 'ok' } satisfies ApplicationRecord)
|
|
3704
4012
|
}
|
|
3705
4013
|
|
|
3706
4014
|
/**
|
|
@@ -3728,8 +4036,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} reportApplicationServerError(error: unknow
|
|
|
3728
4036
|
} catch {
|
|
3729
4037
|
message = '[ERROR] Application server failed'
|
|
3730
4038
|
}
|
|
3731
|
-
process.stderr.write(\`\${message}\\n\`)
|
|
3732
4039
|
process.exitCode = 1
|
|
4040
|
+
process.stderr.write(\`\${message}\\n\`)
|
|
3733
4041
|
}
|
|
3734
4042
|
`
|
|
3735
4043
|
}),
|
|
@@ -3739,176 +4047,106 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} reportApplicationServerError(error: unknow
|
|
|
3739
4047
|
summary: "The application server implementation.",
|
|
3740
4048
|
category: "source",
|
|
3741
4049
|
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'
|
|
4050
|
+
content: `import type { DispatcherInterface } from '@orkestrel/router'
|
|
4051
|
+
import type { ConnectionInfo, ServerInterface } from '@orkestrel/server'
|
|
4052
|
+
import type {
|
|
4053
|
+
ApplicationServerInterface,
|
|
4054
|
+
ApplicationServerOptions,
|
|
4055
|
+
ApplicationState,
|
|
4056
|
+
} from './types.js'
|
|
4057
|
+
import { createBoundary, createDeadline, createSecurity } from '@orkestrel/middleware'
|
|
4058
|
+
import { createServer } from '@orkestrel/server'
|
|
4059
|
+
import { ApplicationServerError } from './errors.js'
|
|
3757
4060
|
import {
|
|
3758
4061
|
parseApplicationHost,
|
|
3759
4062
|
parseApplicationPort,
|
|
3760
4063
|
parseApplicationServerOptions,
|
|
3761
4064
|
parseApplicationStartTimeout,
|
|
3762
4065
|
} from './parsers.js'
|
|
4066
|
+
import { createApplicationDispatcher } from './routes.js'
|
|
3763
4067
|
|
|
3764
|
-
/** A repeat-safe
|
|
4068
|
+
/** A repeat-safe application server composed from the installed server substrate. */
|
|
3765
4069
|
${EXPORT_KEYWORD} class ApplicationServer implements ApplicationServerInterface {
|
|
3766
4070
|
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>()
|
|
4071
|
+
readonly #dispatcher: DispatcherInterface<ApplicationState>
|
|
4072
|
+
readonly #server: ServerInterface<ApplicationState>
|
|
3774
4073
|
|
|
3775
4074
|
constructor(options: ApplicationServerOptions = {}) {
|
|
3776
4075
|
const parsed = parseApplicationServerOptions(options)
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
4076
|
+
const server = parsed.server
|
|
4077
|
+
const host = parseApplicationHost(
|
|
4078
|
+
server?.host === undefined ? process.env.APP_HOST : server.host,
|
|
3780
4079
|
)
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
parsed.timeout === undefined ? process.env.APP_START_TIMEOUT : parsed.timeout,
|
|
4080
|
+
const port = parseApplicationPort(
|
|
4081
|
+
server?.port === undefined ? process.env.APP_PORT : server.port,
|
|
3784
4082
|
)
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
this
|
|
3789
|
-
this.#
|
|
3790
|
-
this.#server
|
|
3791
|
-
|
|
4083
|
+
const timeout = parseApplicationStartTimeout(
|
|
4084
|
+
server?.timeout === undefined ? process.env.APP_START_TIMEOUT : server.timeout,
|
|
4085
|
+
)
|
|
4086
|
+
this.host = host
|
|
4087
|
+
this.#dispatcher = createApplicationDispatcher()
|
|
4088
|
+
this.#server = createServer<ApplicationState>({
|
|
4089
|
+
dispatcher: this.#dispatcher,
|
|
4090
|
+
state: ApplicationServer.#state,
|
|
4091
|
+
middleware: [
|
|
4092
|
+
createBoundary<ApplicationState>(),
|
|
4093
|
+
createSecurity<ApplicationState>(),
|
|
4094
|
+
createDeadline<ApplicationState>({ ms: timeout }),
|
|
4095
|
+
],
|
|
4096
|
+
host,
|
|
4097
|
+
port,
|
|
4098
|
+
timeouts: { start: timeout },
|
|
4099
|
+
})
|
|
3792
4100
|
}
|
|
3793
4101
|
|
|
3794
|
-
|
|
3795
|
-
return
|
|
4102
|
+
static #state(connection: ConnectionInfo): ApplicationState {
|
|
4103
|
+
return { connection }
|
|
3796
4104
|
}
|
|
3797
4105
|
|
|
3798
|
-
get
|
|
3799
|
-
return this.#server.
|
|
4106
|
+
get port(): number | undefined {
|
|
4107
|
+
return this.#server.port
|
|
3800
4108
|
}
|
|
3801
4109
|
|
|
3802
|
-
get
|
|
3803
|
-
|
|
3804
|
-
return \`http://\${hostname}:\${this.port}\`
|
|
4110
|
+
get status(): ApplicationServerInterface['status'] {
|
|
4111
|
+
return this.#server.status
|
|
3805
4112
|
}
|
|
3806
4113
|
|
|
3807
|
-
|
|
3808
|
-
const
|
|
3809
|
-
|
|
3810
|
-
const
|
|
3811
|
-
|
|
3812
|
-
return queued
|
|
4114
|
+
get url(): string | undefined {
|
|
4115
|
+
const port = this.port
|
|
4116
|
+
if (port === undefined) return undefined
|
|
4117
|
+
const hostname = this.host.includes(':') ? \`[\${this.host}]\` : this.host
|
|
4118
|
+
return \`http://\${hostname}:\${port}\`
|
|
3813
4119
|
}
|
|
3814
4120
|
|
|
3815
|
-
|
|
3816
|
-
for (const controller of this.#starts) {
|
|
3817
|
-
this.#stopped.add(controller)
|
|
3818
|
-
controller.abort()
|
|
3819
|
-
}
|
|
3820
|
-
return this.#queue(this.#stop.bind(this))
|
|
3821
|
-
}
|
|
3822
|
-
|
|
3823
|
-
#queue(operation: () => Promise<void>): Promise<void> {
|
|
3824
|
-
const queued = this.#transition.then(operation, operation)
|
|
3825
|
-
this.#transition = queued.then(
|
|
3826
|
-
() => undefined,
|
|
3827
|
-
() => undefined,
|
|
3828
|
-
)
|
|
3829
|
-
return queued
|
|
3830
|
-
}
|
|
3831
|
-
|
|
3832
|
-
async #start(controller: AbortController, signal?: AbortSignal): Promise<void> {
|
|
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
|
-
}
|
|
4121
|
+
async start(signal?: AbortSignal): Promise<void> {
|
|
3838
4122
|
try {
|
|
3839
|
-
|
|
3840
|
-
throw new ApplicationServerError('LIFECYCLE', 'Application server startup was cancelled', {
|
|
3841
|
-
cause: signal.reason,
|
|
3842
|
-
})
|
|
3843
|
-
}
|
|
3844
|
-
await this.#listen(controller, signal)
|
|
4123
|
+
await this.#server.start(signal)
|
|
3845
4124
|
} catch (error) {
|
|
3846
|
-
|
|
3847
|
-
if (isApplicationServerError(error)) throw error
|
|
3848
|
-
throw new ApplicationServerError('LIFECYCLE', 'Failed to inspect startup signal', {
|
|
4125
|
+
throw new ApplicationServerError('LIFECYCLE', 'Failed to start application server', {
|
|
3849
4126
|
cause: error,
|
|
3850
4127
|
})
|
|
3851
4128
|
}
|
|
3852
4129
|
}
|
|
3853
4130
|
|
|
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)
|
|
4131
|
+
async stop(): Promise<void> {
|
|
3862
4132
|
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 })
|
|
4133
|
+
await this.#server.stop()
|
|
3872
4134
|
} catch (error) {
|
|
3873
|
-
throw new ApplicationServerError('LIFECYCLE', 'Failed to
|
|
4135
|
+
throw new ApplicationServerError('LIFECYCLE', 'Failed to stop application server', {
|
|
3874
4136
|
cause: error,
|
|
3875
4137
|
})
|
|
3876
|
-
} finally {
|
|
3877
|
-
clearTimeout(timer)
|
|
3878
|
-
if (relay !== undefined && signal !== undefined) {
|
|
3879
|
-
signal.removeEventListener('abort', relay)
|
|
3880
|
-
}
|
|
3881
|
-
}
|
|
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
4138
|
}
|
|
3887
|
-
this.#port = address.port
|
|
3888
4139
|
}
|
|
3889
4140
|
|
|
3890
|
-
|
|
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
|
-
}
|
|
3901
|
-
|
|
3902
|
-
async #close(): Promise<void> {
|
|
4141
|
+
async destroy(): Promise<void> {
|
|
3903
4142
|
try {
|
|
3904
|
-
|
|
3905
|
-
this.#server.closeIdleConnections()
|
|
3906
|
-
this.#server.closeAllConnections()
|
|
3907
|
-
await closed
|
|
4143
|
+
await this.#server.destroy()
|
|
3908
4144
|
} catch (error) {
|
|
3909
|
-
throw new ApplicationServerError('LIFECYCLE', 'Failed to
|
|
4145
|
+
throw new ApplicationServerError('LIFECYCLE', 'Failed to destroy application server', {
|
|
3910
4146
|
cause: error,
|
|
3911
4147
|
})
|
|
4148
|
+
} finally {
|
|
4149
|
+
this.#dispatcher.destroy()
|
|
3912
4150
|
}
|
|
3913
4151
|
}
|
|
3914
4152
|
}
|
|
@@ -3931,16 +4169,17 @@ import { ApplicationServerRunner } from './ApplicationServerRunner.js'
|
|
|
3931
4169
|
/**
|
|
3932
4170
|
* Create a stopped application server.
|
|
3933
4171
|
*
|
|
3934
|
-
* @param options - Optional
|
|
4172
|
+
* @param options - Optional grouped server overrides.
|
|
3935
4173
|
* @returns A lifecycle-safe application server.
|
|
3936
4174
|
*
|
|
3937
4175
|
* @example
|
|
3938
4176
|
* \`\`\`ts
|
|
3939
4177
|
* import { createApplicationServer } from '@app/server'
|
|
3940
4178
|
*
|
|
3941
|
-
* ${CONST_KEYWORD} server = createApplicationServer({ port: 0 })
|
|
4179
|
+
* ${CONST_KEYWORD} server = createApplicationServer({ server: { port: 0 } })
|
|
3942
4180
|
* await server.start()
|
|
3943
4181
|
* await server.stop()
|
|
4182
|
+
* await server.destroy()
|
|
3944
4183
|
* \`\`\`
|
|
3945
4184
|
*/
|
|
3946
4185
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationServer(
|
|
@@ -3952,21 +4191,21 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} createApplicationServer(
|
|
|
3952
4191
|
/**
|
|
3953
4192
|
* Start a process-owned application server.
|
|
3954
4193
|
*
|
|
3955
|
-
* @param options - Optional
|
|
4194
|
+
* @param options - Optional grouped server overrides.
|
|
3956
4195
|
* @returns The runner that owns signals and provides explicit asynchronous cleanup.
|
|
3957
4196
|
*
|
|
3958
4197
|
* @example
|
|
3959
4198
|
* \`\`\`ts
|
|
3960
4199
|
* import { startApplicationServer } from '@app/server'
|
|
3961
4200
|
*
|
|
3962
|
-
* ${CONST_KEYWORD} runner = startApplicationServer({ port: 0 })
|
|
4201
|
+
* ${CONST_KEYWORD} runner = startApplicationServer({ server: { port: 0 } })
|
|
3963
4202
|
* await runner.stop()
|
|
3964
4203
|
* \`\`\`
|
|
3965
4204
|
*/
|
|
3966
4205
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationServer(
|
|
3967
4206
|
options: ApplicationServerOptions = {},
|
|
3968
4207
|
): ApplicationServerRunnerInterface {
|
|
3969
|
-
const runner = new ApplicationServerRunner(options)
|
|
4208
|
+
const runner = new ApplicationServerRunner(new ApplicationServer(options))
|
|
3970
4209
|
runner.start()
|
|
3971
4210
|
return runner
|
|
3972
4211
|
}
|
|
@@ -3977,57 +4216,144 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationServer(
|
|
|
3977
4216
|
name: "appServerRunner",
|
|
3978
4217
|
summary: "The application server process lifecycle owner.",
|
|
3979
4218
|
category: "source",
|
|
3980
|
-
placeholders: Object.freeze([
|
|
3981
|
-
|
|
4219
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4220
|
+
name: "nameImport",
|
|
4221
|
+
description: "The selected layer import for APP_NAME."
|
|
4222
|
+
})]),
|
|
4223
|
+
content: `import type { EmitterInterface } from '@orkestrel/emitter'
|
|
4224
|
+
import type {
|
|
3982
4225
|
ApplicationServerInterface,
|
|
3983
|
-
|
|
4226
|
+
ApplicationServerRunnerEventMap,
|
|
3984
4227
|
ApplicationServerRunnerInterface,
|
|
4228
|
+
ApplicationServerRunnerOptions,
|
|
3985
4229
|
} from './types.js'
|
|
3986
|
-
import {
|
|
4230
|
+
import { Emitter } from '@orkestrel/emitter'
|
|
4231
|
+
{{nameImport}}
|
|
4232
|
+
import { ApplicationServerError } from './errors.js'
|
|
3987
4233
|
import { reportApplicationServerError } from './handlers.js'
|
|
3988
4234
|
|
|
3989
4235
|
/** Own process signals and startup failure handling for one application server. */
|
|
3990
4236
|
${EXPORT_KEYWORD} class ApplicationServerRunner implements ApplicationServerRunnerInterface {
|
|
4237
|
+
readonly #emitter: Emitter<ApplicationServerRunnerEventMap>
|
|
3991
4238
|
readonly #server: ApplicationServerInterface
|
|
3992
|
-
readonly #
|
|
4239
|
+
readonly #handler: () => void
|
|
4240
|
+
#controller: AbortController | undefined
|
|
4241
|
+
#queue: Promise<void> = Promise.resolve()
|
|
4242
|
+
#stopping: Promise<void> | undefined
|
|
4243
|
+
#announcement: number | undefined
|
|
3993
4244
|
#generation = 0
|
|
3994
4245
|
#started = false
|
|
3995
4246
|
|
|
3996
|
-
constructor(options:
|
|
3997
|
-
this.#
|
|
3998
|
-
|
|
4247
|
+
constructor(server: ApplicationServerInterface, options: ApplicationServerRunnerOptions = {}) {
|
|
4248
|
+
this.#emitter = new Emitter({
|
|
4249
|
+
...(options.on === undefined ? {} : { on: options.on }),
|
|
4250
|
+
...(options.error === undefined ? {} : { error: options.error }),
|
|
4251
|
+
})
|
|
4252
|
+
this.#server = server
|
|
4253
|
+
this.#handler = this.#shutdown.bind(this)
|
|
4254
|
+
this.#emitter.on('ready', this.#announce.bind(this))
|
|
4255
|
+
this.#emitter.on('fail', reportApplicationServerError)
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
get emitter(): EmitterInterface<ApplicationServerRunnerEventMap> {
|
|
4259
|
+
return this.#emitter
|
|
3999
4260
|
}
|
|
4000
4261
|
|
|
4001
4262
|
start(): void {
|
|
4002
4263
|
if (this.#started) return
|
|
4003
4264
|
this.#started = true
|
|
4265
|
+
this.#stopping = undefined
|
|
4004
4266
|
const generation = ++this.#generation
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4267
|
+
const controller = new AbortController()
|
|
4268
|
+
this.#controller = controller
|
|
4269
|
+
process.once('SIGINT', this.#handler)
|
|
4270
|
+
process.once('SIGTERM', this.#handler)
|
|
4271
|
+
this.#queue = this.#queue.then(this.#begin.bind(this, generation, controller))
|
|
4008
4272
|
}
|
|
4009
4273
|
|
|
4010
4274
|
stop(): Promise<void> {
|
|
4011
|
-
this.#
|
|
4275
|
+
if (this.#stopping !== undefined) return this.#stopping
|
|
4276
|
+
// Aborting first lets an in-flight substrate startup settle before stop inspects its state,
|
|
4277
|
+
// and the generation bump is what discards that abort's rejection while leaving a genuine
|
|
4278
|
+
// stop failure reportable. The shared queue then keeps a subsequent restart behind this complete
|
|
4279
|
+
// shutdown; concurrent callers join one substrate stop through #stopping, and the trailing catch
|
|
4280
|
+
// discards nothing — it only keeps one failed stop from wedging every later one.
|
|
4281
|
+
++this.#generation
|
|
4282
|
+
this.#controller?.abort()
|
|
4012
4283
|
this.#release()
|
|
4013
|
-
|
|
4284
|
+
const stopping = this.#queue.then(() => this.#server.stop())
|
|
4285
|
+
this.#stopping = stopping
|
|
4286
|
+
this.#queue = stopping.catch(() => undefined)
|
|
4287
|
+
void stopping.then(this.#finishStop.bind(this, stopping), this.#rejectStop.bind(this, stopping))
|
|
4288
|
+
return stopping
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
async #begin(generation: number, controller: AbortController): Promise<void> {
|
|
4292
|
+
if (generation !== this.#generation || !this.#started) return
|
|
4293
|
+
try {
|
|
4294
|
+
await this.#server.start(controller.signal)
|
|
4295
|
+
await this.#ready(generation)
|
|
4296
|
+
} catch (error) {
|
|
4297
|
+
this.#fail(generation, error)
|
|
4298
|
+
}
|
|
4014
4299
|
}
|
|
4015
4300
|
|
|
4016
4301
|
#fail(generation: number, error: unknown): void {
|
|
4017
4302
|
if (generation !== this.#generation) return
|
|
4018
4303
|
this.#release()
|
|
4019
|
-
|
|
4304
|
+
this.#emitter.emit('fail', error)
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
#finishStop(stopping: Promise<void>): void {
|
|
4308
|
+
if (this.#stopping === stopping) this.#stopping = undefined
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
#rejectStop(stopping: Promise<void>, error: unknown): void {
|
|
4312
|
+
this.#finishStop(stopping)
|
|
4313
|
+
this.#emitter.emit('fail', error)
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
async #ready(generation: number): Promise<void> {
|
|
4317
|
+
if (generation !== this.#generation || !this.#started) return
|
|
4318
|
+
const url = this.#server.url
|
|
4319
|
+
if (url === undefined) {
|
|
4320
|
+
const failure = new ApplicationServerError(
|
|
4321
|
+
'LIFECYCLE',
|
|
4322
|
+
'Application server did not expose a URL after successful startup',
|
|
4323
|
+
)
|
|
4324
|
+
try {
|
|
4325
|
+
await this.#server.stop()
|
|
4326
|
+
} catch (error) {
|
|
4327
|
+
this.#fail(generation, error)
|
|
4328
|
+
return
|
|
4329
|
+
}
|
|
4330
|
+
this.#fail(generation, failure)
|
|
4331
|
+
return
|
|
4332
|
+
}
|
|
4333
|
+
this.#announcement = generation
|
|
4334
|
+
this.#emitter.emit('ready', url)
|
|
4335
|
+
this.#announcement = undefined
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
#announce(url: string): void {
|
|
4339
|
+
if (this.#announcement !== this.#generation) return
|
|
4340
|
+
try {
|
|
4341
|
+
process.stderr.write(\`[READY] \${APP_NAME} \${url}\\n\`)
|
|
4342
|
+
} catch (error) {
|
|
4343
|
+
const stopped = this.stop()
|
|
4344
|
+
const generation = this.#generation
|
|
4345
|
+
void stopped.then(this.#fail.bind(this, generation, error), () => undefined)
|
|
4346
|
+
}
|
|
4020
4347
|
}
|
|
4021
4348
|
|
|
4022
4349
|
#shutdown(): void {
|
|
4023
4350
|
const stopped = this.stop()
|
|
4024
|
-
|
|
4025
|
-
void stopped.catch(this.#fail.bind(this, generation))
|
|
4351
|
+
void stopped.catch(() => undefined)
|
|
4026
4352
|
}
|
|
4027
4353
|
|
|
4028
4354
|
#release(): void {
|
|
4029
|
-
process.off('SIGINT', this.#
|
|
4030
|
-
process.off('SIGTERM', this.#
|
|
4355
|
+
process.off('SIGINT', this.#handler)
|
|
4356
|
+
process.off('SIGTERM', this.#handler)
|
|
4031
4357
|
this.#started = false
|
|
4032
4358
|
}
|
|
4033
4359
|
}
|
|
@@ -4043,6 +4369,7 @@ ${EXPORT_KEYWORD} class ApplicationServerRunner implements ApplicationServerRunn
|
|
|
4043
4369
|
export * from './constants.js'
|
|
4044
4370
|
export * from './errors.js'
|
|
4045
4371
|
export * from './parsers.js'
|
|
4372
|
+
export * from './routes.js'
|
|
4046
4373
|
export * from './handlers.js'
|
|
4047
4374
|
export * from './ApplicationServer.js'
|
|
4048
4375
|
export * from './ApplicationServerRunner.js'
|
|
@@ -4067,10 +4394,16 @@ try {
|
|
|
4067
4394
|
setup: Object.freeze({
|
|
4068
4395
|
id: "setup",
|
|
4069
4396
|
name: "setup",
|
|
4070
|
-
summary: "The generated-minimal `tests/setup.ts`
|
|
4397
|
+
summary: "The generated-minimal `tests/setup.ts` shared test helpers.",
|
|
4071
4398
|
category: "tests",
|
|
4072
|
-
placeholders: Object.freeze([
|
|
4073
|
-
|
|
4399
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4400
|
+
name: "eventImport",
|
|
4401
|
+
description: "The optional emitter types used by application server tests."
|
|
4402
|
+
}), Object.freeze({
|
|
4403
|
+
name: "eventHelper",
|
|
4404
|
+
description: "The optional typed event waiter used by application server tests."
|
|
4405
|
+
})]),
|
|
4406
|
+
content: `{{eventImport}}// ── Call recorder (a real callback, not a mock) ──────────────────────────────
|
|
4074
4407
|
//
|
|
4075
4408
|
// The test rules require a recording callback when a test only needs to count calls or inspect arguments:
|
|
4076
4409
|
// recorder — a real listener that records every invocation — rather than a test-
|
|
@@ -4110,12 +4443,64 @@ ${EXPORT_KEYWORD} function createRecorder<TArgs extends readonly unknown[]>(): T
|
|
|
4110
4443
|
},
|
|
4111
4444
|
}
|
|
4112
4445
|
}
|
|
4113
|
-
|
|
4446
|
+
{{eventHelper}}
|
|
4114
4447
|
/** Whether a repository-relative Vue SFC belongs to the private browser application. */
|
|
4115
4448
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserVuePath(path: string): boolean {
|
|
4116
4449
|
const normalized = path.replaceAll('\\\\', '/')
|
|
4117
4450
|
return normalized.startsWith('app/browser/')
|
|
4118
4451
|
}
|
|
4452
|
+
`
|
|
4453
|
+
}),
|
|
4454
|
+
configTest: Object.freeze({
|
|
4455
|
+
id: "configTest",
|
|
4456
|
+
name: "configTest",
|
|
4457
|
+
summary: "The generated root Vite configuration behavior test.",
|
|
4458
|
+
category: "tests",
|
|
4459
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4460
|
+
name: "imports",
|
|
4461
|
+
description: "The machinery-aware root configuration imports."
|
|
4462
|
+
}), Object.freeze({
|
|
4463
|
+
name: "cases",
|
|
4464
|
+
description: "The machinery-aware root configuration cases."
|
|
4465
|
+
})]),
|
|
4466
|
+
content: `{{imports}}
|
|
4467
|
+
|
|
4468
|
+
describe('root Vite configuration', () => {
|
|
4469
|
+
it('keeps workspace paths physically contained', () => {
|
|
4470
|
+
const root = resolveWorkspacePath('.')
|
|
4471
|
+
const source = resolveWorkspacePath('src')
|
|
4472
|
+
const parent = resolveWorkspacePath('..')
|
|
4473
|
+
|
|
4474
|
+
expect(workspacePath(root)).toBe('')
|
|
4475
|
+
expect(workspacePath(source)).toBe('src')
|
|
4476
|
+
expect(workspacePath(parent)).toBeUndefined()
|
|
4477
|
+
expect(containedPath(root, source)).toBe(true)
|
|
4478
|
+
expect(containedPath(root, parent)).toBe(false)
|
|
4479
|
+
})
|
|
4480
|
+
|
|
4481
|
+
it('enforces environment direction for paths and module sources', () => {
|
|
4482
|
+
expect(environmentPathError('src/core', 'app/core/index.ts')).toBe(
|
|
4483
|
+
'Published modules cannot depend on private application modules',
|
|
4484
|
+
)
|
|
4485
|
+
expect(environmentPathError('app/core', 'src/browser/index.ts')).toBe(
|
|
4486
|
+
'Core modules must remain host-independent',
|
|
4487
|
+
)
|
|
4488
|
+
expect(environmentPathError('app/browser', 'src/server/index.ts')).toBe(
|
|
4489
|
+
'Browser modules cannot depend on Node or server-only modules',
|
|
4490
|
+
)
|
|
4491
|
+
expect(environmentPathError('app/server', 'src/browser/index.ts')).toBe(
|
|
4492
|
+
'Server modules cannot depend on Vue or browser-only modules',
|
|
4493
|
+
)
|
|
4494
|
+
expect(environmentPathError('app/browser', 'src/core/index.ts')).toBeUndefined()
|
|
4495
|
+
expect(environmentSourceError('src/core', 'node:path')).toBe(
|
|
4496
|
+
'Core modules must remain host-independent',
|
|
4497
|
+
)
|
|
4498
|
+
expect(environmentSourceError('app/server', 'vue')).toBe(
|
|
4499
|
+
'Server modules cannot depend on Vue or browser-only modules',
|
|
4500
|
+
)
|
|
4501
|
+
expect(environmentSourceError('app/browser', '@app/core')).toBeUndefined()
|
|
4502
|
+
}){{cases}}
|
|
4503
|
+
})
|
|
4119
4504
|
`
|
|
4120
4505
|
}),
|
|
4121
4506
|
policyTest: Object.freeze({
|
|
@@ -4123,32 +4508,17 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} isBrowserVuePath(path: string): boolean {
|
|
|
4123
4508
|
name: "policyTest",
|
|
4124
4509
|
summary: "The generated repository filename-policy test.",
|
|
4125
4510
|
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'
|
|
4511
|
+
placeholders: Object.freeze([Object.freeze({
|
|
4512
|
+
name: "vuePolicyImport",
|
|
4513
|
+
description: "The optional official Vue SFC compiler import."
|
|
4514
|
+
}), Object.freeze({
|
|
4515
|
+
name: "workspacePolicyAssertion",
|
|
4516
|
+
description: "The formatter-stable workspace policy assertion."
|
|
4517
|
+
})]),
|
|
4518
|
+
content: `import { globSync } from 'node:fs'
|
|
4149
4519
|
import { describe, expect, it } from 'vitest'
|
|
4150
4520
|
import { isBrowserVuePath } from './setup.js'
|
|
4151
|
-
import { inspectCodingWorkspace } from './setupPolicy.js'{{
|
|
4521
|
+
import { inspectCodingWorkspace } from './setupPolicy.js'{{vuePolicyImport}}
|
|
4152
4522
|
|
|
4153
4523
|
describe('repository coding law', () => {
|
|
4154
4524
|
it('keeps Vue single-file components exclusively in browser environments', () => {
|
|
@@ -4159,7 +4529,7 @@ describe('repository coding law', () => {
|
|
|
4159
4529
|
|
|
4160
4530
|
it('enforces source placement, exports, readonly contracts, and syntax law', () => {
|
|
4161
4531
|
{{workspacePolicyAssertion}}
|
|
4162
|
-
})
|
|
4532
|
+
})
|
|
4163
4533
|
})
|
|
4164
4534
|
`
|
|
4165
4535
|
}),
|
|
@@ -4179,6 +4549,7 @@ ${IMPORT_KEYWORD} { createServer } from 'node:http'
|
|
|
4179
4549
|
/** One real application child process plus its captured diagnostic output. */
|
|
4180
4550
|
${EXPORT_KEYWORD} interface ApplicationProcessInterface {
|
|
4181
4551
|
readonly child: ChildProcess
|
|
4552
|
+
readonly ready: Promise<void>
|
|
4182
4553
|
output(): string
|
|
4183
4554
|
}
|
|
4184
4555
|
|
|
@@ -4218,11 +4589,17 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} buildApplicationServer(): void {
|
|
|
4218
4589
|
}
|
|
4219
4590
|
}
|
|
4220
4591
|
|
|
4221
|
-
/**
|
|
4222
|
-
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD}
|
|
4592
|
+
/** Start a real Node server bound to one loopback port, or 0 for an ephemeral one. */
|
|
4593
|
+
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} startLoopbackServer(port: number): Promise<Server> {
|
|
4223
4594
|
const server = createServer()
|
|
4224
|
-
server.listen(
|
|
4595
|
+
server.listen(port, '127.0.0.1')
|
|
4225
4596
|
await once(server, 'listening')
|
|
4597
|
+
return server
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
/** Reserve and release a real loopback port for an immediate child-process bind. */
|
|
4601
|
+
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<number> {
|
|
4602
|
+
const server = await startLoopbackServer(0)
|
|
4226
4603
|
const address = server.address()
|
|
4227
4604
|
if (address === null || typeof address === 'string') {
|
|
4228
4605
|
await stopNodeServer(server)
|
|
@@ -4232,23 +4609,6 @@ ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<numbe
|
|
|
4232
4609
|
return address.port
|
|
4233
4610
|
}
|
|
4234
4611
|
|
|
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
4612
|
/** Start the built application entry as a real child process. */
|
|
4253
4613
|
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationProcess(
|
|
4254
4614
|
port: number,
|
|
@@ -4265,16 +4625,20 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} startApplicationProcess(
|
|
|
4265
4625
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4266
4626
|
})
|
|
4267
4627
|
const output: string[] = []
|
|
4628
|
+
const readiness = Promise.withResolvers<void>()
|
|
4268
4629
|
child.stdout?.setEncoding('utf8')
|
|
4269
4630
|
child.stderr?.setEncoding('utf8')
|
|
4270
4631
|
child.stdout?.on('data', (chunk: unknown) => {
|
|
4271
4632
|
if (typeof chunk === 'string') output.push(chunk)
|
|
4272
4633
|
})
|
|
4273
4634
|
child.stderr?.on('data', (chunk: unknown) => {
|
|
4274
|
-
if (typeof chunk
|
|
4635
|
+
if (typeof chunk !== 'string') return
|
|
4636
|
+
output.push(chunk)
|
|
4637
|
+
if (output.join('').includes('[READY] ')) readiness.resolve()
|
|
4275
4638
|
})
|
|
4276
4639
|
return {
|
|
4277
4640
|
child,
|
|
4641
|
+
ready: readiness.promise,
|
|
4278
4642
|
output() {
|
|
4279
4643
|
return output.join('')
|
|
4280
4644
|
},
|
|
@@ -4439,15 +4803,28 @@ describe('create{{pascal}}', () => {
|
|
|
4439
4803
|
name: "appCoreTest",
|
|
4440
4804
|
summary: "The host-independent application test.",
|
|
4441
4805
|
category: "tests",
|
|
4442
|
-
placeholders: Object.freeze([
|
|
4806
|
+
placeholders: Object.freeze([
|
|
4807
|
+
Object.freeze({
|
|
4808
|
+
name: "guardImport",
|
|
4809
|
+
description: "The optional shared-record guard test import."
|
|
4810
|
+
}),
|
|
4811
|
+
Object.freeze({
|
|
4812
|
+
name: "readImport",
|
|
4813
|
+
description: "The optional shared health-boundary read test import."
|
|
4814
|
+
}),
|
|
4815
|
+
Object.freeze({
|
|
4816
|
+
name: "boundary",
|
|
4817
|
+
description: "The optional shared health-boundary contract test."
|
|
4818
|
+
})
|
|
4819
|
+
]),
|
|
4443
4820
|
content: `import {
|
|
4444
4821
|
APP_NAME,
|
|
4445
4822
|
ApplicationError,
|
|
4446
4823
|
createApplication,
|
|
4447
4824
|
isApplicationError,
|
|
4448
|
-
MAX_APPLICATION_NAME_INPUT_LENGTH,
|
|
4825
|
+
{{guardImport}} MAX_APPLICATION_NAME_INPUT_LENGTH,
|
|
4449
4826
|
parseApplicationName,
|
|
4450
|
-
} from '@app/core'
|
|
4827
|
+
{{readImport}}} from '@app/core'
|
|
4451
4828
|
import { describe, expect, it } from 'vitest'
|
|
4452
4829
|
|
|
4453
4830
|
describe('createApplication', () => {
|
|
@@ -4484,7 +4861,14 @@ describe('createApplication', () => {
|
|
|
4484
4861
|
revocable.revoke()
|
|
4485
4862
|
expect(isApplicationError(revocable.proxy)).toBe(false)
|
|
4486
4863
|
})
|
|
4487
|
-
|
|
4864
|
+
|
|
4865
|
+
it('refuses a foreign application error', () => {
|
|
4866
|
+
const foreign = new Error('foreign')
|
|
4867
|
+
foreign.name = 'ApplicationError'
|
|
4868
|
+
|
|
4869
|
+
expect(isApplicationError(foreign)).toBe(false)
|
|
4870
|
+
})
|
|
4871
|
+
}){{boundary}}
|
|
4488
4872
|
`
|
|
4489
4873
|
}),
|
|
4490
4874
|
appBrowserTest: Object.freeze({
|
|
@@ -4492,18 +4876,36 @@ describe('createApplication', () => {
|
|
|
4492
4876
|
name: "appBrowserTest",
|
|
4493
4877
|
summary: "The real-browser application mount test.",
|
|
4494
4878
|
category: "tests",
|
|
4495
|
-
placeholders: Object.freeze([
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4879
|
+
placeholders: Object.freeze([
|
|
4880
|
+
Object.freeze({
|
|
4881
|
+
name: "browserTestNameImport",
|
|
4882
|
+
description: "The layer-correct browser APP_NAME test import."
|
|
4883
|
+
}),
|
|
4884
|
+
Object.freeze({
|
|
4885
|
+
name: "showcaseImport",
|
|
4886
|
+
description: "The optional showcase factory test import."
|
|
4887
|
+
}),
|
|
4888
|
+
Object.freeze({
|
|
4889
|
+
name: "entryImport",
|
|
4890
|
+
description: "The optional seeder and boundary-entry test imports."
|
|
4891
|
+
}),
|
|
4892
|
+
Object.freeze({
|
|
4893
|
+
name: "showcase",
|
|
4894
|
+
description: "The optional real-browser showcase mount test."
|
|
4895
|
+
}),
|
|
4896
|
+
Object.freeze({
|
|
4897
|
+
name: "boundary",
|
|
4898
|
+
description: "The optional real-browser boundary degradation test."
|
|
4899
|
+
})
|
|
4900
|
+
]),
|
|
4499
4901
|
content: `{{browserTestNameImport}}
|
|
4500
4902
|
import {
|
|
4501
4903
|
BrowserApplicationError,
|
|
4502
4904
|
createBrowserApplication,
|
|
4503
|
-
isBrowserApplicationError,
|
|
4905
|
+
{{showcaseImport}} isBrowserApplicationError,
|
|
4504
4906
|
MAX_BROWSER_APPLICATION_NAME_INPUT_LENGTH,
|
|
4505
4907
|
parseBrowserApplicationOptions,
|
|
4506
|
-
} from '@app/browser'
|
|
4908
|
+
{{entryImport}}} from '@app/browser'
|
|
4507
4909
|
import { buildElement } from '../../setupBrowser.js'
|
|
4508
4910
|
import { describe, expect, it } from 'vitest'
|
|
4509
4911
|
|
|
@@ -4587,313 +4989,205 @@ describe('createBrowserApplication', () => {
|
|
|
4587
4989
|
expect(isBrowserApplicationError(new Error('plain'))).toBe(false)
|
|
4588
4990
|
expect(isBrowserApplicationError(revocable.proxy)).toBe(false)
|
|
4589
4991
|
})
|
|
4590
|
-
})
|
|
4992
|
+
}){{showcase}}{{boundary}}
|
|
4591
4993
|
`
|
|
4592
4994
|
}),
|
|
4593
4995
|
appServerTest: Object.freeze({
|
|
4594
4996
|
id: "appServerTest",
|
|
4595
4997
|
name: "appServerTest",
|
|
4596
|
-
summary: "The real
|
|
4998
|
+
summary: "The real dispatcher, server-substrate, and runner integration test.",
|
|
4597
4999
|
category: "tests",
|
|
4598
|
-
placeholders: Object.freeze([
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
5000
|
+
placeholders: Object.freeze([
|
|
5001
|
+
Object.freeze({
|
|
5002
|
+
name: "testNameImport",
|
|
5003
|
+
description: "The layer-correct APP_NAME and health-contract test import."
|
|
5004
|
+
}),
|
|
5005
|
+
Object.freeze({
|
|
5006
|
+
name: "serverImport",
|
|
5007
|
+
description: "The application server test import, minus any relocated health contract."
|
|
5008
|
+
}),
|
|
5009
|
+
Object.freeze({
|
|
5010
|
+
name: "boundary",
|
|
5011
|
+
description: "The optional real-server shared-boundary test."
|
|
5012
|
+
})
|
|
5013
|
+
]),
|
|
4602
5014
|
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'
|
|
5015
|
+
{{serverImport}}
|
|
4614
5016
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
4615
5017
|
import {
|
|
4616
5018
|
buildApplicationServer,
|
|
4617
5019
|
reserveLoopbackPort,
|
|
4618
5020
|
startApplicationProcess,
|
|
5021
|
+
startLoopbackServer,
|
|
4619
5022
|
stopNodeServer,
|
|
4620
5023
|
waitForApplicationProcess,
|
|
4621
|
-
waitForApplicationResponse,
|
|
4622
|
-
waitForLoopbackResponse,
|
|
4623
|
-
waitForSocketClose,
|
|
4624
5024
|
} from '../../setupServer.js'
|
|
5025
|
+
import { createRecorder, waitForEvent } from '../../setup.js'
|
|
4625
5026
|
|
|
4626
5027
|
beforeAll(() => buildApplicationServer(), 60_000)
|
|
4627
5028
|
|
|
4628
|
-
describe('
|
|
4629
|
-
|
|
4630
|
-
|
|
5029
|
+
describe('application dispatcher', () => {
|
|
5030
|
+
const state = { connection: { encrypted: false } }
|
|
5031
|
+
|
|
5032
|
+
it('returns the typed shared application record for the health route', async () => {
|
|
5033
|
+
const dispatcher = createApplicationDispatcher()
|
|
4631
5034
|
try {
|
|
4632
|
-
|
|
4633
|
-
|
|
5035
|
+
const response = await dispatcher.handle(
|
|
5036
|
+
new Request(\`http://application.test\${APP_HEALTH_PATH}\`),
|
|
5037
|
+
state,
|
|
5038
|
+
)
|
|
4634
5039
|
|
|
4635
|
-
const response = await fetch(server.url)
|
|
4636
5040
|
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
5041
|
expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
|
|
5042
|
+
expect(APP_HEALTH_PATH).toBe('/health')
|
|
4640
5043
|
} 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()
|
|
5044
|
+
dispatcher.destroy()
|
|
4662
5045
|
}
|
|
4663
5046
|
})
|
|
4664
5047
|
|
|
4665
|
-
it('
|
|
4666
|
-
const
|
|
5048
|
+
it('distinguishes a wrong method from a missing route', async () => {
|
|
5049
|
+
const dispatcher = createApplicationDispatcher()
|
|
4667
5050
|
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' }),
|
|
5051
|
+
const method = await dispatcher.handle(
|
|
5052
|
+
new Request(\`http://application.test\${APP_HEALTH_PATH}\`, { method: 'POST' }),
|
|
5053
|
+
state,
|
|
4703
5054
|
)
|
|
4704
|
-
|
|
4705
|
-
expect(createApplicationServer({ host: '::1' }).url).toBe('http://[::1]:3000')
|
|
4706
|
-
})
|
|
5055
|
+
const missing = await dispatcher.handle(new Request('http://application.test/missing'), state)
|
|
4707
5056
|
|
|
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' }))
|
|
5057
|
+
expect(method.status).toBe(405)
|
|
5058
|
+
expect(method.headers.get('allow')).toContain(APP_HEALTH_METHOD)
|
|
5059
|
+
expect(missing.status).toBe(404)
|
|
4725
5060
|
} 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
|
|
5061
|
+
dispatcher.destroy()
|
|
4732
5062
|
}
|
|
4733
5063
|
})
|
|
5064
|
+
})
|
|
4734
5065
|
|
|
4735
|
-
|
|
5066
|
+
describe('ApplicationServer', () => {
|
|
5067
|
+
it('composes the real substrate on loopback with security and repeatable lifecycle', async () => {
|
|
4736
5068
|
const server = createApplicationServer({
|
|
4737
|
-
host: '127.0.0.1',
|
|
4738
|
-
port: 0,
|
|
4739
|
-
timeout: 1_000,
|
|
5069
|
+
server: { host: '127.0.0.1', port: 0, timeout: 1_000 },
|
|
4740
5070
|
})
|
|
4741
|
-
const controller = new AbortController()
|
|
4742
|
-
controller.abort(new Error('cancelled by test'))
|
|
4743
5071
|
try {
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
expect(
|
|
4747
|
-
expect(server.
|
|
4748
|
-
|
|
4749
|
-
await server.stop()
|
|
5072
|
+
expect(server.host).toBe('127.0.0.1')
|
|
5073
|
+
expect(server.port).toBeUndefined()
|
|
5074
|
+
expect(server.url).toBeUndefined()
|
|
5075
|
+
expect(server.status).toBe('idle')
|
|
4750
5076
|
await server.start()
|
|
4751
|
-
expect(
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
await
|
|
4763
|
-
code: 'LIFECYCLE',
|
|
4764
|
-
})
|
|
4765
|
-
expect(server.listening).toBe(false)
|
|
5077
|
+
expect(server.status).toBe('listening')
|
|
5078
|
+
expect(server.port).toEqual(expect.any(Number))
|
|
5079
|
+
const firstUrl = server.url
|
|
5080
|
+
if (firstUrl === undefined) throw new Error('Expected a bound application URL')
|
|
5081
|
+
|
|
5082
|
+
const first = await fetch(\`\${firstUrl}\${APP_HEALTH_PATH}\`)
|
|
5083
|
+
expect(first.status).toBe(200)
|
|
5084
|
+
expect(first.headers.get('x-content-type-options')).toBe('nosniff')
|
|
5085
|
+
expect(first.headers.get('x-frame-options')).toBe('DENY')
|
|
5086
|
+
expect(first.headers.get('content-security-policy')).not.toBeNull()
|
|
5087
|
+
expect(first.headers.get('x-request-id')).not.toBeNull()
|
|
5088
|
+
expect(await first.json()).toEqual({ name: APP_NAME, status: 'ok' })
|
|
4766
5089
|
|
|
4767
|
-
await server.start()
|
|
4768
|
-
expect((await fetch(server.url)).status).toBe(200)
|
|
4769
|
-
} finally {
|
|
4770
5090
|
await server.stop()
|
|
4771
|
-
}
|
|
4772
|
-
})
|
|
4773
|
-
|
|
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
5091
|
await server.stop()
|
|
4794
|
-
|
|
4795
|
-
expect(
|
|
4796
|
-
|
|
4797
|
-
await server.stop()
|
|
4798
|
-
}
|
|
4799
|
-
})
|
|
5092
|
+
expect(server.port).toBeUndefined()
|
|
5093
|
+
expect(server.url).toBeUndefined()
|
|
5094
|
+
expect(server.status).toBe('stopped')
|
|
4800
5095
|
|
|
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
5096
|
await server.start()
|
|
4806
|
-
const
|
|
5097
|
+
const secondUrl = server.url
|
|
5098
|
+
if (secondUrl === undefined) throw new Error('Expected a rebound application URL')
|
|
5099
|
+
const second = await fetch(\`\${secondUrl}\${APP_HEALTH_PATH}\`)
|
|
5100
|
+
expect(second.status).toBe(200)
|
|
4807
5101
|
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)
|
|
5102
|
+
await server.destroy()
|
|
5103
|
+
await server.destroy()
|
|
5104
|
+
expect(server.status).toBe('stopped')
|
|
4815
5105
|
} finally {
|
|
4816
|
-
await server.
|
|
4817
|
-
await stopNodeServer(occupant)
|
|
5106
|
+
await server.destroy()
|
|
4818
5107
|
}
|
|
4819
5108
|
})
|
|
4820
5109
|
|
|
4821
|
-
it('
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
} finally {
|
|
4832
|
-
await server.stop()
|
|
5110
|
+
it('rejects invalid grouped host and port options before binding', () => {
|
|
5111
|
+
for (const value of [
|
|
5112
|
+
{ server: { host: ' ' } },
|
|
5113
|
+
{ server: { port: Number.NaN } },
|
|
5114
|
+
{ server: { port: -1 } },
|
|
5115
|
+
{ server: { timeout: 0 } },
|
|
5116
|
+
]) {
|
|
5117
|
+
expect(() => Reflect.apply(createApplicationServer, undefined, [value])).toThrow(
|
|
5118
|
+
expect.objectContaining({ code: 'CONFIG' }),
|
|
5119
|
+
)
|
|
4833
5120
|
}
|
|
4834
5121
|
})
|
|
5122
|
+
})
|
|
4835
5123
|
|
|
4836
|
-
|
|
4837
|
-
|
|
5124
|
+
describe('ApplicationServerRunner', () => {
|
|
5125
|
+
it('serves a real request after a restart queued during shutdown', async () => {
|
|
5126
|
+
const port = await reserveLoopbackPort()
|
|
5127
|
+
const code = process.exitCode
|
|
5128
|
+
const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
|
|
5129
|
+
const runner = new ApplicationServerRunner(server)
|
|
4838
5130
|
try {
|
|
4839
|
-
|
|
4840
|
-
const
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
5131
|
+
process.exitCode = undefined
|
|
5132
|
+
const first = waitForEvent(runner.emitter, 'ready')
|
|
5133
|
+
runner.start()
|
|
5134
|
+
await first
|
|
5135
|
+
const stopped = runner.stop()
|
|
5136
|
+
const second = waitForEvent(runner.emitter, 'ready')
|
|
5137
|
+
runner.start()
|
|
5138
|
+
await stopped
|
|
4845
5139
|
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
5140
|
+
const [url] = await second
|
|
5141
|
+
const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
|
|
5142
|
+
expect(response.status).toBe(200)
|
|
5143
|
+
expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
|
|
5144
|
+
const stopping = runner.stop()
|
|
5145
|
+
expect(runner.stop()).toBe(stopping)
|
|
5146
|
+
await stopping
|
|
5147
|
+
expect(process.exitCode).toBeUndefined()
|
|
4852
5148
|
} finally {
|
|
4853
|
-
|
|
5149
|
+
process.exitCode = code
|
|
5150
|
+
await runner.stop()
|
|
4854
5151
|
}
|
|
4855
5152
|
})
|
|
4856
5153
|
|
|
4857
|
-
it('
|
|
4858
|
-
const
|
|
4859
|
-
const
|
|
5154
|
+
it('cancels a startup still in flight so an immediate stop leaves the port free', async () => {
|
|
5155
|
+
const port = await reserveLoopbackPort()
|
|
5156
|
+
const ready = createRecorder<[url: string]>()
|
|
5157
|
+
const failed = createRecorder<[error: unknown]>()
|
|
5158
|
+
const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
|
|
5159
|
+
const runner = new ApplicationServerRunner(server, {
|
|
5160
|
+
on: { ready: ready.handler, fail: failed.handler },
|
|
5161
|
+
})
|
|
4860
5162
|
try {
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
}
|
|
4868
|
-
const overflow = connect({ host: server.host, port: server.port })
|
|
5163
|
+
runner.start()
|
|
5164
|
+
await runner.stop()
|
|
5165
|
+
expect(ready.count).toBe(0)
|
|
5166
|
+
expect(failed.count).toBe(0)
|
|
5167
|
+
|
|
5168
|
+
const released = await startLoopbackServer(port)
|
|
4869
5169
|
try {
|
|
4870
|
-
|
|
4871
|
-
await once(overflow, 'connect')
|
|
4872
|
-
await closed
|
|
4873
|
-
expect(overflow.destroyed).toBe(true)
|
|
5170
|
+
expect(released.listening).toBe(true)
|
|
4874
5171
|
} finally {
|
|
4875
|
-
|
|
5172
|
+
await stopNodeServer(released)
|
|
4876
5173
|
}
|
|
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
5174
|
} finally {
|
|
4885
|
-
|
|
4886
|
-
await server.stop()
|
|
5175
|
+
await runner.stop()
|
|
4887
5176
|
}
|
|
4888
5177
|
})
|
|
4889
5178
|
|
|
4890
|
-
it('
|
|
5179
|
+
it('announces readiness exactly once before the first response and releases on SIGTERM', async () => {
|
|
4891
5180
|
const port = await reserveLoopbackPort()
|
|
4892
5181
|
const application = startApplicationProcess(port)
|
|
4893
5182
|
try {
|
|
4894
|
-
|
|
5183
|
+
await application.ready
|
|
5184
|
+
const announcement = \`[READY] \${APP_NAME} http://127.0.0.1:\${port}\\n\`
|
|
5185
|
+
expect(application.output()).toBe(announcement)
|
|
5186
|
+
|
|
5187
|
+
const response = await fetch(\`http://127.0.0.1:\${port}\${APP_HEALTH_PATH}\`)
|
|
4895
5188
|
expect(response.status).toBe(200)
|
|
4896
5189
|
expect(await response.json()).toEqual({ name: APP_NAME, status: 'ok' })
|
|
5190
|
+
expect(application.output().split('[READY]')).toHaveLength(2)
|
|
4897
5191
|
|
|
4898
5192
|
expect(application.child.kill('SIGTERM')).toBe(true)
|
|
4899
5193
|
const exited = await waitForApplicationProcess(application)
|
|
@@ -4903,10 +5197,8 @@ describe('ApplicationServer', () => {
|
|
|
4903
5197
|
: { code: 0, signal: null },
|
|
4904
5198
|
)
|
|
4905
5199
|
|
|
4906
|
-
const released =
|
|
5200
|
+
const released = await startLoopbackServer(port)
|
|
4907
5201
|
try {
|
|
4908
|
-
released.listen(port, '127.0.0.1')
|
|
4909
|
-
await once(released, 'listening')
|
|
4910
5202
|
expect(released.listening).toBe(true)
|
|
4911
5203
|
} finally {
|
|
4912
5204
|
await stopNodeServer(released)
|
|
@@ -4919,105 +5211,81 @@ describe('ApplicationServer', () => {
|
|
|
4919
5211
|
}
|
|
4920
5212
|
})
|
|
4921
5213
|
|
|
4922
|
-
it('
|
|
5214
|
+
it('never announces readiness when a real bind fails', async () => {
|
|
4923
5215
|
const port = await reserveLoopbackPort()
|
|
4924
|
-
const owner =
|
|
4925
|
-
owner.listen(port, '127.0.0.1')
|
|
4926
|
-
await once(owner, 'listening')
|
|
5216
|
+
const owner = await startLoopbackServer(port)
|
|
4927
5217
|
const application = startApplicationProcess(port)
|
|
4928
5218
|
try {
|
|
4929
5219
|
const exited = await waitForApplicationProcess(application)
|
|
4930
5220
|
expect(exited).toEqual({ code: 1, signal: null })
|
|
4931
5221
|
expect(application.output()).toBe('[LIFECYCLE] Application server lifecycle failed\\n')
|
|
5222
|
+
expect(application.output()).not.toContain('[READY]')
|
|
4932
5223
|
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
5224
|
expect(owner.listening).toBe(true)
|
|
4937
5225
|
} finally {
|
|
4938
|
-
if (application.child.exitCode === null && application.child.signalCode === null) {
|
|
4939
|
-
application.child.kill('SIGKILL')
|
|
4940
|
-
await waitForApplicationProcess(application)
|
|
4941
|
-
}
|
|
4942
5226
|
await stopNodeServer(owner)
|
|
4943
5227
|
}
|
|
4944
5228
|
})
|
|
4945
5229
|
|
|
4946
|
-
it('
|
|
4947
|
-
const secret = 'SENTINEL_SECRET/invalid'
|
|
4948
|
-
const application = startApplicationProcess(0, { APP_HOST: secret })
|
|
4949
|
-
const exited = await waitForApplicationProcess(application)
|
|
4950
|
-
expect(exited).toEqual({ code: 1, signal: null })
|
|
4951
|
-
expect(application.output()).toBe('[CONFIG] Application server configuration failed\\n')
|
|
4952
|
-
expect(application.output()).not.toContain(secret)
|
|
4953
|
-
expect(application.output()).not.toContain('context')
|
|
4954
|
-
expect(application.output()).not.toContain('cause')
|
|
4955
|
-
expect(application.output()).not.toContain('at ')
|
|
4956
|
-
})
|
|
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
|
|
5230
|
+
it('isolates a throwing fail listener, releases ownership, and starts again', async () => {
|
|
4996
5231
|
const port = await reserveLoopbackPort()
|
|
4997
|
-
const owner =
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5232
|
+
const owner = await startLoopbackServer(port)
|
|
5233
|
+
const interrupts = process.listenerCount('SIGINT')
|
|
5234
|
+
const terminations = process.listenerCount('SIGTERM')
|
|
5235
|
+
// The reporter owns the process exit code, so this in-process failure records the
|
|
5236
|
+
// code it set and then restores whatever the surrounding run had.
|
|
5237
|
+
const code = process.exitCode
|
|
5238
|
+
const errors = createRecorder<[error: unknown, event: string]>()
|
|
5239
|
+
const server = createApplicationServer({ server: { host: '127.0.0.1', port } })
|
|
5240
|
+
const runner = new ApplicationServerRunner(server, {
|
|
5241
|
+
on: {
|
|
5242
|
+
fail: () => {
|
|
5243
|
+
throw new Error('listener boom')
|
|
5244
|
+
},
|
|
5245
|
+
},
|
|
5246
|
+
error: errors.handler,
|
|
5247
|
+
})
|
|
5001
5248
|
try {
|
|
5249
|
+
process.exitCode = undefined
|
|
5250
|
+
const failed = waitForEvent(runner.emitter, 'fail')
|
|
5002
5251
|
runner.start()
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
runner.start()
|
|
5006
|
-
await stopNodeServer(owner)
|
|
5007
|
-
await stopping
|
|
5252
|
+
expect(process.listenerCount('SIGINT')).toBe(interrupts + 1)
|
|
5253
|
+
expect(process.listenerCount('SIGTERM')).toBe(terminations + 1)
|
|
5008
5254
|
|
|
5009
|
-
const
|
|
5255
|
+
const [failure] = await failed
|
|
5256
|
+
expect(failure).toEqual(expect.objectContaining({ code: 'LIFECYCLE' }))
|
|
5257
|
+
expect(errors.count).toBe(1)
|
|
5258
|
+
expect(errors.calls[0]?.[1]).toBe('fail')
|
|
5259
|
+
expect(process.listenerCount('SIGINT')).toBe(interrupts)
|
|
5260
|
+
expect(process.listenerCount('SIGTERM')).toBe(terminations)
|
|
5261
|
+
expect(process.exitCode).toBe(1)
|
|
5262
|
+
|
|
5263
|
+
await stopNodeServer(owner)
|
|
5264
|
+
const ready = waitForEvent(runner.emitter, 'ready')
|
|
5265
|
+
runner.start()
|
|
5266
|
+
const [url] = await ready
|
|
5267
|
+
const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
|
|
5010
5268
|
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
5269
|
} finally {
|
|
5270
|
+
process.exitCode = code
|
|
5015
5271
|
await runner.stop()
|
|
5016
5272
|
await stopNodeServer(owner)
|
|
5017
|
-
process.exitCode = previousExitCode
|
|
5018
5273
|
}
|
|
5019
5274
|
})
|
|
5020
|
-
|
|
5275
|
+
|
|
5276
|
+
it('redacts malformed environment values from executable diagnostics', async () => {
|
|
5277
|
+
const secret = 'SENTINEL_SECRET/invalid'
|
|
5278
|
+
const application = startApplicationProcess(0, { APP_HOST: secret })
|
|
5279
|
+
const exited = await waitForApplicationProcess(application)
|
|
5280
|
+
expect(exited).toEqual({ code: 1, signal: null })
|
|
5281
|
+
expect(application.output()).toBe('[CONFIG] Application server configuration failed\\n')
|
|
5282
|
+
expect(application.output()).not.toContain(secret)
|
|
5283
|
+
expect(application.output()).not.toContain('[READY]')
|
|
5284
|
+
expect(application.output()).not.toContain('context')
|
|
5285
|
+
expect(application.output()).not.toContain('cause')
|
|
5286
|
+
expect(application.output()).not.toContain('at ')
|
|
5287
|
+
})
|
|
5288
|
+
}){{boundary}}
|
|
5021
5289
|
`
|
|
5022
5290
|
}),
|
|
5023
5291
|
appServerParsersTest: Object.freeze({
|
|
@@ -5041,6 +5309,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
5041
5309
|
|
|
5042
5310
|
describe('application environment parsers', () => {
|
|
5043
5311
|
it('accepts port boundaries and trims a host', () => {
|
|
5312
|
+
expect(parseApplicationPort(3000)).toBe(3000)
|
|
5044
5313
|
expect(parseApplicationPort('0')).toBe(0)
|
|
5045
5314
|
expect(parseApplicationPort('65535')).toBe(65_535)
|
|
5046
5315
|
expect(parseApplicationPort(' 3000 ')).toBe(3000)
|
|
@@ -5052,6 +5321,19 @@ describe('application environment parsers', () => {
|
|
|
5052
5321
|
expect(parseApplicationStartTimeout(String(MAX_APP_START_TIMEOUT))).toBe(MAX_APP_START_TIMEOUT)
|
|
5053
5322
|
})
|
|
5054
5323
|
|
|
5324
|
+
it('never coerces hostile numeric objects through toString', () => {
|
|
5325
|
+
let calls = 0
|
|
5326
|
+
const hostile = {
|
|
5327
|
+
toString() {
|
|
5328
|
+
calls += 1
|
|
5329
|
+
return '3000'
|
|
5330
|
+
},
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5333
|
+
expect(() => parseApplicationPort(hostile)).toThrow(ApplicationServerError)
|
|
5334
|
+
expect(calls).toBe(0)
|
|
5335
|
+
})
|
|
5336
|
+
|
|
5055
5337
|
it.each(['', '-1', '65536', '1.5', '+1', '0x10', '1e3', 'NaN', 'Infinity'])(
|
|
5056
5338
|
'rejects hostile APP_PORT value %s',
|
|
5057
5339
|
(value) => {
|
|
@@ -5093,7 +5375,15 @@ describe('application environment parsers', () => {
|
|
|
5093
5375
|
expect(() => parseApplicationHost(value)).toThrow(expect.objectContaining({ code: 'CONFIG' }))
|
|
5094
5376
|
})
|
|
5095
5377
|
|
|
5096
|
-
it
|
|
5378
|
+
it('parses the exact grouped server record including its timeout leaf', () => {
|
|
5379
|
+
expect(
|
|
5380
|
+
parseApplicationServerOptions({
|
|
5381
|
+
server: { host: ' 127.0.0.1 ', port: 0, timeout: 250 },
|
|
5382
|
+
}),
|
|
5383
|
+
).toEqual({ server: { host: '127.0.0.1', port: 0, timeout: 250 } })
|
|
5384
|
+
})
|
|
5385
|
+
|
|
5386
|
+
it.each([null, 42, [], { server: null }, { server: { port: [42] } }, { server: { timeout: 0 } }])(
|
|
5097
5387
|
'rejects hostile option container or leaf value %#',
|
|
5098
5388
|
(value) => {
|
|
5099
5389
|
expect(() => parseApplicationServerOptions(value)).toThrow(
|
|
@@ -5103,11 +5393,14 @@ describe('application environment parsers', () => {
|
|
|
5103
5393
|
)
|
|
5104
5394
|
|
|
5105
5395
|
it.each([
|
|
5106
|
-
Object.create({ host: '0.0.0.0' }),
|
|
5396
|
+
Object.create({ server: { host: '0.0.0.0' } }),
|
|
5107
5397
|
{ post: 0 },
|
|
5108
5398
|
new Date(),
|
|
5109
|
-
{
|
|
5110
|
-
|
|
5399
|
+
{ server: Object.create({ host: '0.0.0.0' }) },
|
|
5400
|
+
{ server: { post: 0 } },
|
|
5401
|
+
{ server: { [Symbol('host')]: '0.0.0.0' } },
|
|
5402
|
+
Object.defineProperty({}, 'server', { get: () => ({ host: '0.0.0.0' }) }),
|
|
5403
|
+
{ server: Object.defineProperty({}, 'host', { get: () => '0.0.0.0' }) },
|
|
5111
5404
|
new Proxy(
|
|
5112
5405
|
{},
|
|
5113
5406
|
{
|
|
@@ -5116,6 +5409,16 @@ describe('application environment parsers', () => {
|
|
|
5116
5409
|
},
|
|
5117
5410
|
},
|
|
5118
5411
|
),
|
|
5412
|
+
{
|
|
5413
|
+
server: new Proxy(
|
|
5414
|
+
{},
|
|
5415
|
+
{
|
|
5416
|
+
ownKeys: () => {
|
|
5417
|
+
throw new Error('hostile nested ownKeys trap')
|
|
5418
|
+
},
|
|
5419
|
+
},
|
|
5420
|
+
),
|
|
5421
|
+
},
|
|
5119
5422
|
])(
|
|
5120
5423
|
'rejects inherited, unknown, symbolic, accessor, instance, and hostile proxy options %#',
|
|
5121
5424
|
(value) => {
|
|
@@ -5125,6 +5428,19 @@ describe('application environment parsers', () => {
|
|
|
5125
5428
|
},
|
|
5126
5429
|
)
|
|
5127
5430
|
|
|
5431
|
+
it('does not invoke grouped option accessors', () => {
|
|
5432
|
+
let reads = 0
|
|
5433
|
+
const server = Object.defineProperty({}, 'timeout', {
|
|
5434
|
+
get() {
|
|
5435
|
+
reads += 1
|
|
5436
|
+
return 250
|
|
5437
|
+
},
|
|
5438
|
+
})
|
|
5439
|
+
|
|
5440
|
+
expect(() => parseApplicationServerOptions({ server })).toThrow(ApplicationServerError)
|
|
5441
|
+
expect(reads).toBe(0)
|
|
5442
|
+
})
|
|
5443
|
+
|
|
5128
5444
|
it('never reflects a hostile symbolic option name into diagnostics', () => {
|
|
5129
5445
|
let caught: unknown
|
|
5130
5446
|
try {
|
|
@@ -5174,9 +5490,7 @@ ${IMPORT_KEYWORD} { createSource, parseManifest } from '@orkestrel/guide'
|
|
|
5174
5490
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_ROOT = fileURLToPath(new URL('../', import.meta.url))
|
|
5175
5491
|
|
|
5176
5492
|
/** 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
|
-
])
|
|
5493
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze({{walkDirs}})
|
|
5180
5494
|
|
|
5181
5495
|
{{specifiers}}
|
|
5182
5496
|
|
|
@@ -5568,7 +5882,9 @@ function devDependenciesFor(spec) {
|
|
|
5568
5882
|
return {
|
|
5569
5883
|
...dependencies,
|
|
5570
5884
|
...spec.src.includes("browser") ? SOURCE_BROWSER_DEV_DEPENDENCIES : {},
|
|
5885
|
+
...spec.app.length > 0 ? APP_DEV_DEPENDENCIES : {},
|
|
5571
5886
|
...spec.app.includes("browser") ? APP_BROWSER_DEV_DEPENDENCIES : {},
|
|
5887
|
+
...spec.app.includes("server") ? APP_SERVER_DEV_DEPENDENCIES : {},
|
|
5572
5888
|
...spec.showcase ? { "vite-plugin-singlefile": "^2.3.3" } : {},
|
|
5573
5889
|
...spec.bin ? { "@vitest/browser-playwright": SOURCE_BROWSER_DEV_DEPENDENCIES["@vitest/browser-playwright"] } : {}
|
|
5574
5890
|
};
|
|
@@ -5623,6 +5939,7 @@ function packageManifest(spec) {
|
|
|
5623
5939
|
...hasSource || spec.bin ? ["npm run test:src"] : [],
|
|
5624
5940
|
...spec.app.length > 0 ? ["npm run test:app"] : [],
|
|
5625
5941
|
"npm run test:policy",
|
|
5942
|
+
"npm run test:config",
|
|
5626
5943
|
"npm run test:guides"
|
|
5627
5944
|
].join(" && ");
|
|
5628
5945
|
if (hasSource || spec.bin) {
|
|
@@ -5638,6 +5955,7 @@ function packageManifest(spec) {
|
|
|
5638
5955
|
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
5956
|
}
|
|
5640
5957
|
scripts["test:policy"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy";
|
|
5958
|
+
scripts["test:config"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project config";
|
|
5641
5959
|
scripts["test:guides"] = "vitest run --config vite.config.ts --reporter=dot --project guides";
|
|
5642
5960
|
scripts.build = [
|
|
5643
5961
|
"npm run clean",
|
|
@@ -5658,7 +5976,7 @@ function packageManifest(spec) {
|
|
|
5658
5976
|
if (spec.showcase) {
|
|
5659
5977
|
scripts.showcase = `vite --config ${SHOWCASE_CONFIG_PATH}`;
|
|
5660
5978
|
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";
|
|
5979
|
+
scripts.show = "npm run format && npm run build:showcase && npm run copy dist/showcase/index.html demo/showcase.html";
|
|
5662
5980
|
}
|
|
5663
5981
|
}
|
|
5664
5982
|
if (spec.app.includes("server")) {
|
|
@@ -5808,7 +6126,7 @@ function viteMachinery(src, app = [], bin = false, showcase = false) {
|
|
|
5808
6126
|
* @example
|
|
5809
6127
|
* ```ts
|
|
5810
6128
|
* viteProjectRegistrations(['core'], [], { integration: true })
|
|
5811
|
-
* // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'guides' }, { project: 'integration' }]
|
|
6129
|
+
* // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'config' }, { project: 'guides' }, { project: 'integration' }]
|
|
5812
6130
|
* ```
|
|
5813
6131
|
*/
|
|
5814
6132
|
function viteProjectRegistrations(src, app = [], facts = {}) {
|
|
@@ -5831,7 +6149,7 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
|
|
|
5831
6149
|
});
|
|
5832
6150
|
if (environment === "server") registrations.push({ project: "appServer" });
|
|
5833
6151
|
}
|
|
5834
|
-
registrations.push({ project: "policy" }, { project: "guides" });
|
|
6152
|
+
registrations.push({ project: "policy" }, { project: "config" }, { project: "guides" });
|
|
5835
6153
|
if (facts.bin === true) registrations.push({ project: "srcBin" });
|
|
5836
6154
|
if (facts.integration === true) registrations.push({ project: "integration" });
|
|
5837
6155
|
if (facts.service === true) registrations.push({ project: "service" });
|
|
@@ -5841,7 +6159,7 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
|
|
|
5841
6159
|
* Render the one ordered proof and structural-axis project definition block.
|
|
5842
6160
|
*
|
|
5843
6161
|
* @param facts - Optional structural facts.
|
|
5844
|
-
* @returns Policy, guides, then selected axis project definitions, separated by one blank line.
|
|
6162
|
+
* @returns Policy, config, guides, then selected axis project definitions, separated by one blank line.
|
|
5845
6163
|
*
|
|
5846
6164
|
* @example
|
|
5847
6165
|
* ```ts
|
|
@@ -5849,7 +6167,11 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
|
|
|
5849
6167
|
* ```
|
|
5850
6168
|
*/
|
|
5851
6169
|
function viteProjectDefinitions(facts = {}) {
|
|
5852
|
-
const definitions = [
|
|
6170
|
+
const definitions = [
|
|
6171
|
+
policyViteProject(),
|
|
6172
|
+
configViteProject(),
|
|
6173
|
+
guidesViteProject()
|
|
6174
|
+
];
|
|
5853
6175
|
if (facts.bin === true) definitions.push(binViteProject());
|
|
5854
6176
|
if (facts.integration === true) definitions.push(integrationViteProject(facts));
|
|
5855
6177
|
if (facts.service === true) definitions.push(serviceViteProject());
|
|
@@ -5915,9 +6237,11 @@ import { chromium } from 'playwright'
|
|
|
5915
6237
|
` : "";
|
|
5916
6238
|
const vueImports = needsVue ? `import vue from '@vitejs/plugin-vue'
|
|
5917
6239
|
import { parse as parseVue } from 'vue/compiler-sfc'
|
|
6240
|
+
import { parseStartTag } from '@orkestrel/html'
|
|
5918
6241
|
` : "";
|
|
5919
6242
|
const showcaseImports = needsShowcase ? `import { viteSingleFile } from 'vite-plugin-singlefile'
|
|
5920
6243
|
` : "";
|
|
6244
|
+
const showcaseHashImport = needsShowcase ? "import { createHash } from 'node:crypto'\n" : "";
|
|
5921
6245
|
const viteTypeImports = needsVue ? `import type {
|
|
5922
6246
|
CSSOptions,
|
|
5923
6247
|
HtmlAssetSource,
|
|
@@ -6879,12 +7203,40 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_POLICY =
|
|
|
6879
7203
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_META =
|
|
6880
7204
|
'<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
|
|
6881
7205
|
HTML_SECURITY_POLICY +
|
|
7206
|
+
'"\\n\\t\\t/>'${needsShowcase ? `
|
|
7207
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_POLICY =
|
|
7208
|
+
"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'"
|
|
7209
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_SECURITY_META =
|
|
7210
|
+
'<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
|
|
7211
|
+
SHOWCASE_SECURITY_POLICY +
|
|
7212
|
+
'"\\n\\t\\t/>'
|
|
7213
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_BUILD_SECURITY_POLICY =
|
|
7214
|
+
"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'"
|
|
7215
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} SHOWCASE_BUILD_SECURITY_META =
|
|
7216
|
+
'<meta\\n\\t\\t\\thttp-equiv="Content-Security-Policy"\\n\\t\\t\\tcontent="' +
|
|
7217
|
+
SHOWCASE_BUILD_SECURITY_POLICY +
|
|
6882
7218
|
'"\\n\\t\\t/>'
|
|
6883
|
-
|
|
6884
|
-
|
|
7219
|
+
` : ""}
|
|
7220
|
+
|
|
7221
|
+
${EXPORT_KEYWORD} function hasSecurityPrologue(html: string, security: string): boolean {
|
|
7222
|
+
const normalized = html.replaceAll('\\r\\n', '\\n')
|
|
7223
|
+
const doctype = '<!doctype html>\\n'
|
|
7224
|
+
if (!normalized.startsWith(doctype)) return false
|
|
7225
|
+
const root = parseStartTag(normalized, doctype.length)
|
|
7226
|
+
return (
|
|
7227
|
+
root !== undefined &&
|
|
7228
|
+
root.name === 'html' &&
|
|
7229
|
+
!root.slashed &&
|
|
7230
|
+
normalized.startsWith('\\n\\t<head>\\n\\t\\t' + security + '\\n', root.next)
|
|
7231
|
+
)
|
|
7232
|
+
}
|
|
6885
7233
|
|
|
6886
|
-
${EXPORT_KEYWORD} function maskIgnoredHtml(
|
|
6887
|
-
|
|
7234
|
+
${EXPORT_KEYWORD} function maskIgnoredHtml(
|
|
7235
|
+
environmentKeys: ReadonlySet<string>,
|
|
7236
|
+
html: string,
|
|
7237
|
+
security: string,
|
|
7238
|
+
): string {
|
|
7239
|
+
if (!hasSecurityPrologue(html, security)) {
|
|
6888
7240
|
throw new Error(
|
|
6889
7241
|
'[orkestrel-environment-boundary] Browser HTML must preserve the generated security prologue',
|
|
6890
7242
|
)
|
|
@@ -6918,12 +7270,27 @@ ${EXPORT_KEYWORD} function maskIgnoredHtml(environmentKeys: ReadonlySet<string>,
|
|
|
6918
7270
|
)
|
|
6919
7271
|
}
|
|
6920
7272
|
|
|
6921
|
-
${EXPORT_KEYWORD} function
|
|
7273
|
+
${needsShowcase ? `${EXPORT_KEYWORD} function isShowcaseHtmlEntry(filename: string): boolean {
|
|
6922
7274
|
return (
|
|
6923
|
-
physicalPath(filename) ===
|
|
7275
|
+
physicalPath(filename) ===
|
|
7276
|
+
physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/showcase.html'))
|
|
6924
7277
|
)
|
|
6925
7278
|
}
|
|
6926
7279
|
|
|
7280
|
+
` : ""}${EXPORT_KEYWORD} function isBrowserHtmlEntry(filename: string): boolean {
|
|
7281
|
+
${needsShowcase ? `return (
|
|
7282
|
+
isShowcaseHtmlEntry(filename) ||
|
|
7283
|
+
physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
|
|
7284
|
+
)` : `return (
|
|
7285
|
+
physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
|
|
7286
|
+
)`}
|
|
7287
|
+
}${needsShowcase ? `
|
|
7288
|
+
${EXPORT_KEYWORD} function browserHtmlSecurityMeta(filename: string, built: boolean): string | undefined {
|
|
7289
|
+
if (!isBrowserHtmlEntry(filename)) return undefined
|
|
7290
|
+
if (!isShowcaseHtmlEntry(filename)) return HTML_SECURITY_META
|
|
7291
|
+
return built ? SHOWCASE_BUILD_SECURITY_META : SHOWCASE_SECURITY_META
|
|
7292
|
+
}` : ""}
|
|
7293
|
+
|
|
6927
7294
|
${EXPORT_KEYWORD} function prepareHtml(): Plugin {
|
|
6928
7295
|
const environmentKeys = new Set<string>()
|
|
6929
7296
|
return {
|
|
@@ -6940,8 +7307,10 @@ ${EXPORT_KEYWORD} function prepareHtml(): Plugin {
|
|
|
6940
7307
|
transformIndexHtml: {
|
|
6941
7308
|
order: 'pre',
|
|
6942
7309
|
handler(html, context) {
|
|
6943
|
-
|
|
6944
|
-
|
|
7310
|
+
${needsShowcase ? `const security = browserHtmlSecurityMeta(context.filename, false)
|
|
7311
|
+
if (security === undefined) return undefined
|
|
7312
|
+
return maskIgnoredHtml(environmentKeys, html, security)` : `if (!isBrowserHtmlEntry(context.filename)) return undefined
|
|
7313
|
+
return maskIgnoredHtml(environmentKeys, html, HTML_SECURITY_META)`}
|
|
6945
7314
|
},
|
|
6946
7315
|
},
|
|
6947
7316
|
}
|
|
@@ -6965,8 +7334,13 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
|
|
|
6965
7334
|
transformIndexHtml: {
|
|
6966
7335
|
order: 'post',
|
|
6967
7336
|
handler(html, context) {
|
|
6968
|
-
|
|
6969
|
-
|
|
7337
|
+
${needsShowcase ? `const security = browserHtmlSecurityMeta(
|
|
7338
|
+
context.filename,
|
|
7339
|
+
context.bundle !== undefined,
|
|
7340
|
+
)
|
|
7341
|
+
if (security === undefined) return undefined
|
|
7342
|
+
if (!html.includes(security)) {` : `if (!isBrowserHtmlEntry(context.filename)) return undefined
|
|
7343
|
+
if (!html.includes(HTML_SECURITY_META)) {`}
|
|
6970
7344
|
throw new Error(
|
|
6971
7345
|
'[orkestrel-environment-boundary] Browser HTML must retain its security policy',
|
|
6972
7346
|
)
|
|
@@ -6976,26 +7350,49 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
|
|
|
6976
7350
|
}
|
|
6977
7351
|
}
|
|
6978
7352
|
|
|
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 {
|
|
7353
|
+
` : ""}${needsShowcase ? `${EXPORT_KEYWORD} function showcaseHtml(): Plugin {
|
|
6983
7354
|
return {
|
|
6984
7355
|
name: 'orkestrel-showcase-html',
|
|
6985
|
-
enforce: 'post',
|
|
6986
7356
|
transformIndexHtml: {
|
|
6987
7357
|
order: 'post',
|
|
6988
7358
|
handler(html, context) {
|
|
6989
|
-
if (!
|
|
6990
|
-
|
|
6991
|
-
|
|
7359
|
+
if (!isShowcaseHtmlEntry(context.filename) || context.bundle === undefined) {
|
|
7360
|
+
return undefined
|
|
7361
|
+
}
|
|
7362
|
+
if (!html.includes(SHOWCASE_SECURITY_META)) {
|
|
7363
|
+
throw new Error(
|
|
7364
|
+
'[orkestrel-showcase-html] Showcase build did not retain its development security policy',
|
|
7365
|
+
)
|
|
7366
|
+
}
|
|
7367
|
+
const secured = html.replace(SHOWCASE_SECURITY_META, SHOWCASE_BUILD_SECURITY_META)
|
|
7368
|
+
const build = createHash('sha256').update(secured).digest('hex')
|
|
7369
|
+
return {
|
|
7370
|
+
html: secured,
|
|
7371
|
+
tags: [
|
|
7372
|
+
{
|
|
7373
|
+
tag: 'meta',
|
|
7374
|
+
attrs: { name: 'build-id', content: build },
|
|
7375
|
+
injectTo: 'head',
|
|
7376
|
+
},
|
|
7377
|
+
],
|
|
6992
7378
|
}
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
)
|
|
7379
|
+
},
|
|
7380
|
+
},
|
|
7381
|
+
generateBundle: {
|
|
7382
|
+
order: 'post',
|
|
7383
|
+
handler(_options, bundle) {
|
|
7384
|
+
let html: (typeof bundle)[string] | undefined
|
|
7385
|
+
for (const output of Object.values(bundle)) {
|
|
7386
|
+
if (!output.fileName.endsWith('.html')) continue
|
|
7387
|
+
if (html !== undefined) {
|
|
7388
|
+
this.error('[orkestrel-showcase-html] Showcase build emitted multiple HTML entries')
|
|
7389
|
+
}
|
|
7390
|
+
html = output
|
|
7391
|
+
}
|
|
7392
|
+
if (html === undefined) {
|
|
7393
|
+
this.error('[orkestrel-showcase-html] Showcase build did not emit an HTML entry')
|
|
7394
|
+
}
|
|
7395
|
+
html.fileName = 'index.html'
|
|
6999
7396
|
},
|
|
7000
7397
|
},
|
|
7001
7398
|
}
|
|
@@ -7292,7 +7689,7 @@ ${needsBrowser ? `import { isCSSRequest, parseSync, preprocessCSS, transformWith
|
|
|
7292
7689
|
`}import { defineConfig, mergeConfig } from 'vitest/config'
|
|
7293
7690
|
import tsconfig from './tsconfig.json' with { type: 'json' }
|
|
7294
7691
|
import { fileURLToPath, URL } from 'node:url'
|
|
7295
|
-
import { isBuiltin } from 'node:module'
|
|
7692
|
+
${showcaseHashImport}import { isBuiltin } from 'node:module'
|
|
7296
7693
|
import {
|
|
7297
7694
|
${needsBrowser ? " accessSync,\n" : ""} closeSync,
|
|
7298
7695
|
constants as FS_CONSTANTS,
|
|
@@ -7615,7 +8012,7 @@ ${environmentBoundary}`;
|
|
|
7615
8012
|
* ```
|
|
7616
8013
|
*/
|
|
7617
8014
|
function policyViteProject() {
|
|
7618
|
-
return `${EXPORT_KEYWORD} const policy = (
|
|
8015
|
+
return `${EXPORT_KEYWORD} const policy = (options?: UserConfig): UserConfig =>
|
|
7619
8016
|
mergeConfig(
|
|
7620
8017
|
{
|
|
7621
8018
|
resolve,
|
|
@@ -7627,7 +8024,34 @@ function policyViteProject() {
|
|
|
7627
8024
|
browser: { enabled: false },
|
|
7628
8025
|
},
|
|
7629
8026
|
},
|
|
7630
|
-
|
|
8027
|
+
options ?? {},
|
|
8028
|
+
)
|
|
8029
|
+
`;
|
|
8030
|
+
}
|
|
8031
|
+
/**
|
|
8032
|
+
* Build the standalone Node-only root-configuration Vitest project.
|
|
8033
|
+
*
|
|
8034
|
+
* @returns The emitted `config` project definition.
|
|
8035
|
+
*
|
|
8036
|
+
* @example
|
|
8037
|
+
* ```ts
|
|
8038
|
+
* configViteProject().includes("label: 'config'") // true
|
|
8039
|
+
* ```
|
|
8040
|
+
*/
|
|
8041
|
+
function configViteProject() {
|
|
8042
|
+
return `${EXPORT_KEYWORD} const config = (options?: UserConfig): UserConfig =>
|
|
8043
|
+
mergeConfig(
|
|
8044
|
+
{
|
|
8045
|
+
resolve,
|
|
8046
|
+
test: {
|
|
8047
|
+
name: { label: 'config', color: 'yellow' },
|
|
8048
|
+
include: ['tests/config/**/*.test.ts'],
|
|
8049
|
+
setupFiles: ['./tests/setup.ts'],
|
|
8050
|
+
environment: 'node',
|
|
8051
|
+
browser: { enabled: false },
|
|
8052
|
+
},
|
|
8053
|
+
},
|
|
8054
|
+
options ?? {},
|
|
7631
8055
|
)
|
|
7632
8056
|
`;
|
|
7633
8057
|
}
|
|
@@ -7642,7 +8066,7 @@ function policyViteProject() {
|
|
|
7642
8066
|
* ```
|
|
7643
8067
|
*/
|
|
7644
8068
|
function guidesViteProject() {
|
|
7645
|
-
return `${EXPORT_KEYWORD} const guides = (
|
|
8069
|
+
return `${EXPORT_KEYWORD} const guides = (options?: UserConfig): UserConfig =>
|
|
7646
8070
|
mergeConfig(
|
|
7647
8071
|
{
|
|
7648
8072
|
resolve,
|
|
@@ -7655,7 +8079,7 @@ function guidesViteProject() {
|
|
|
7655
8079
|
browser: { enabled: false },
|
|
7656
8080
|
},
|
|
7657
8081
|
},
|
|
7658
|
-
|
|
8082
|
+
options ?? {},
|
|
7659
8083
|
)
|
|
7660
8084
|
`;
|
|
7661
8085
|
}
|
|
@@ -7670,7 +8094,7 @@ function guidesViteProject() {
|
|
|
7670
8094
|
* ```
|
|
7671
8095
|
*/
|
|
7672
8096
|
function binViteProject() {
|
|
7673
|
-
return `${EXPORT_KEYWORD} const srcBin = (
|
|
8097
|
+
return `${EXPORT_KEYWORD} const srcBin = (options?: UserConfig): UserConfig =>
|
|
7674
8098
|
mergeConfig(
|
|
7675
8099
|
{
|
|
7676
8100
|
resolve,
|
|
@@ -7697,7 +8121,7 @@ function binViteProject() {
|
|
|
7697
8121
|
browser: { enabled: false },
|
|
7698
8122
|
},
|
|
7699
8123
|
},
|
|
7700
|
-
|
|
8124
|
+
options ?? {},
|
|
7701
8125
|
)
|
|
7702
8126
|
`;
|
|
7703
8127
|
}
|
|
@@ -7715,7 +8139,7 @@ function binViteProject() {
|
|
|
7715
8139
|
* ```
|
|
7716
8140
|
*/
|
|
7717
8141
|
function integrationViteProject(facts = {}) {
|
|
7718
|
-
return `${EXPORT_KEYWORD} const integration = (
|
|
8142
|
+
return `${EXPORT_KEYWORD} const integration = (options?: UserConfig): UserConfig =>
|
|
7719
8143
|
mergeConfig(
|
|
7720
8144
|
{
|
|
7721
8145
|
resolve,
|
|
@@ -7732,7 +8156,7 @@ ${facts.bin === true && facts.integration === true && facts.global === true ? `
|
|
|
7732
8156
|
fileParallelism: false,
|
|
7733
8157
|
},
|
|
7734
8158
|
},
|
|
7735
|
-
|
|
8159
|
+
options ?? {},
|
|
7736
8160
|
)
|
|
7737
8161
|
`;
|
|
7738
8162
|
}
|
|
@@ -7747,7 +8171,7 @@ ${facts.bin === true && facts.integration === true && facts.global === true ? `
|
|
|
7747
8171
|
* ```
|
|
7748
8172
|
*/
|
|
7749
8173
|
function serviceViteProject() {
|
|
7750
|
-
return `${EXPORT_KEYWORD} const service = (
|
|
8174
|
+
return `${EXPORT_KEYWORD} const service = (options?: UserConfig): UserConfig =>
|
|
7751
8175
|
mergeConfig(
|
|
7752
8176
|
{
|
|
7753
8177
|
resolve,
|
|
@@ -7762,7 +8186,7 @@ function serviceViteProject() {
|
|
|
7762
8186
|
fileParallelism: false,
|
|
7763
8187
|
},
|
|
7764
8188
|
},
|
|
7765
|
-
|
|
8189
|
+
options ?? {},
|
|
7766
8190
|
)
|
|
7767
8191
|
`;
|
|
7768
8192
|
}
|
|
@@ -7789,7 +8213,7 @@ function singleSrcViteConfig(environment, facts = {}) {
|
|
|
7789
8213
|
const renderedTest = renderViteTest(viteProjectRegistrations([environment], [], facts), machinery.browser);
|
|
7790
8214
|
const definitions = viteProjectDefinitions(facts);
|
|
7791
8215
|
if (environment === "browser") return `${header}
|
|
7792
|
-
${EXPORT_KEYWORD} const srcBrowser = (
|
|
8216
|
+
${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
|
|
7793
8217
|
mergeConfig(
|
|
7794
8218
|
{
|
|
7795
8219
|
resolve,
|
|
@@ -7814,7 +8238,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
7814
8238
|
name: { label: 'src:browser', color: 'yellow' },
|
|
7815
8239
|
include: ['tests/src/browser/**/*.test.ts'],
|
|
7816
8240
|
${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
7817
|
-
...(
|
|
8241
|
+
...(options?.test?.browser?.enabled === false
|
|
7818
8242
|
? {}
|
|
7819
8243
|
: {
|
|
7820
8244
|
deps: {
|
|
@@ -7834,7 +8258,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
7834
8258
|
fileParallelism: false,
|
|
7835
8259
|
},
|
|
7836
8260
|
},
|
|
7837
|
-
|
|
8261
|
+
options ?? {},
|
|
7838
8262
|
)
|
|
7839
8263
|
|
|
7840
8264
|
${definitions}
|
|
@@ -7844,7 +8268,7 @@ ${renderedTest}
|
|
|
7844
8268
|
})
|
|
7845
8269
|
`;
|
|
7846
8270
|
return `${header}
|
|
7847
|
-
${EXPORT_KEYWORD} const srcServer = (
|
|
8271
|
+
${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
|
|
7848
8272
|
mergeConfig(
|
|
7849
8273
|
{
|
|
7850
8274
|
resolve,
|
|
@@ -7874,7 +8298,7 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
|
|
|
7874
8298
|
browser: { enabled: false },
|
|
7875
8299
|
},
|
|
7876
8300
|
},
|
|
7877
|
-
|
|
8301
|
+
options ?? {},
|
|
7878
8302
|
)
|
|
7879
8303
|
|
|
7880
8304
|
${definitions}
|
|
@@ -7923,7 +8347,7 @@ function rootViteConfig(src, facts = {}) {
|
|
|
7923
8347
|
if (onlyEnvironment === "browser" || onlyEnvironment === "server") return singleSrcViteConfig(onlyEnvironment, facts);
|
|
7924
8348
|
}
|
|
7925
8349
|
const browserBlock = `
|
|
7926
|
-
${EXPORT_KEYWORD} const srcBrowser = (
|
|
8350
|
+
${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
|
|
7927
8351
|
srcCore(
|
|
7928
8352
|
mergeConfig(
|
|
7929
8353
|
{
|
|
@@ -7947,7 +8371,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
7947
8371
|
include: ['tests/src/browser/**/*.test.ts'],
|
|
7948
8372
|
exclude: ['tests/src/core/**/*.test.ts'],
|
|
7949
8373
|
${facts.global === true ? `globalSetup: ['./${GLOBAL_SETUP_PATH}'],\n\t\t\t\t\t` : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
7950
|
-
...(
|
|
8374
|
+
...(options?.test?.browser?.enabled === false
|
|
7951
8375
|
? {}
|
|
7952
8376
|
: {
|
|
7953
8377
|
deps: {
|
|
@@ -7967,12 +8391,12 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
7967
8391
|
fileParallelism: false,
|
|
7968
8392
|
},
|
|
7969
8393
|
},
|
|
7970
|
-
|
|
8394
|
+
options ?? {},
|
|
7971
8395
|
),
|
|
7972
8396
|
)
|
|
7973
8397
|
`;
|
|
7974
8398
|
const serverBlock = `
|
|
7975
|
-
${EXPORT_KEYWORD} const srcServer = (
|
|
8399
|
+
${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
|
|
7976
8400
|
srcCore(
|
|
7977
8401
|
mergeConfig(
|
|
7978
8402
|
{
|
|
@@ -8010,14 +8434,14 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
|
|
|
8010
8434
|
setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
8011
8435
|
},
|
|
8012
8436
|
},
|
|
8013
|
-
|
|
8437
|
+
options ?? {},
|
|
8014
8438
|
),
|
|
8015
8439
|
)
|
|
8016
8440
|
`;
|
|
8017
8441
|
const blocks = nonCore.map((environment) => environment === "browser" ? browserBlock : serverBlock).join("");
|
|
8018
8442
|
const renderedTest = renderViteTest(viteProjectRegistrations(src, [], facts), machinery.browser);
|
|
8019
8443
|
return `${header}
|
|
8020
|
-
${EXPORT_KEYWORD} const srcCore = (
|
|
8444
|
+
${EXPORT_KEYWORD} const srcCore = (options?: UserConfig): UserConfig =>
|
|
8021
8445
|
mergeConfig(
|
|
8022
8446
|
{
|
|
8023
8447
|
resolve,
|
|
@@ -8035,7 +8459,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
|
|
|
8035
8459
|
browser: { enabled: false },
|
|
8036
8460
|
},
|
|
8037
8461
|
},
|
|
8038
|
-
|
|
8462
|
+
options ?? {},
|
|
8039
8463
|
)
|
|
8040
8464
|
${blocks}
|
|
8041
8465
|
${viteProjectDefinitions(facts)}
|
|
@@ -8065,7 +8489,7 @@ function applicationViteConfig(src, app, facts = {}) {
|
|
|
8065
8489
|
const header = viteHeader(machinery);
|
|
8066
8490
|
const blocks = [];
|
|
8067
8491
|
if (src.includes("core")) blocks.push(`
|
|
8068
|
-
${EXPORT_KEYWORD} const srcCore = (
|
|
8492
|
+
${EXPORT_KEYWORD} const srcCore = (options?: UserConfig): UserConfig =>
|
|
8069
8493
|
mergeConfig(
|
|
8070
8494
|
{
|
|
8071
8495
|
resolve,
|
|
@@ -8080,7 +8504,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
|
|
|
8080
8504
|
browser: { enabled: false },
|
|
8081
8505
|
},
|
|
8082
8506
|
},
|
|
8083
|
-
|
|
8507
|
+
options ?? {},
|
|
8084
8508
|
)
|
|
8085
8509
|
`);
|
|
8086
8510
|
if (src.includes("browser")) {
|
|
@@ -8088,7 +8512,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
|
|
|
8088
8512
|
output: { paths: { '@src/core': '../core/index.js' } },` : "";
|
|
8089
8513
|
const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
|
|
8090
8514
|
blocks.push(`
|
|
8091
|
-
${EXPORT_KEYWORD} const srcBrowser = (
|
|
8515
|
+
${EXPORT_KEYWORD} const srcBrowser = (options?: UserConfig): UserConfig =>
|
|
8092
8516
|
mergeConfig(
|
|
8093
8517
|
{
|
|
8094
8518
|
resolve,
|
|
@@ -8113,7 +8537,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
8113
8537
|
name: { label: 'src:browser', color: 'yellow' },
|
|
8114
8538
|
include: ['tests/src/browser/**/*.test.ts'],
|
|
8115
8539
|
${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
|
-
...(
|
|
8540
|
+
...(options?.test?.browser?.enabled === false
|
|
8117
8541
|
? {}
|
|
8118
8542
|
: {
|
|
8119
8543
|
deps: {
|
|
@@ -8133,7 +8557,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
8133
8557
|
fileParallelism: false,
|
|
8134
8558
|
},
|
|
8135
8559
|
},
|
|
8136
|
-
|
|
8560
|
+
options ?? {},
|
|
8137
8561
|
)
|
|
8138
8562
|
`);
|
|
8139
8563
|
}
|
|
@@ -8154,7 +8578,7 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
|
|
|
8154
8578
|
const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
|
|
8155
8579
|
const formats = hasSourceCore ? "" : "\n formats: ['es', 'cjs'],";
|
|
8156
8580
|
blocks.push(`
|
|
8157
|
-
${EXPORT_KEYWORD} const srcServer = (
|
|
8581
|
+
${EXPORT_KEYWORD} const srcServer = (options?: UserConfig): UserConfig =>
|
|
8158
8582
|
mergeConfig(
|
|
8159
8583
|
{
|
|
8160
8584
|
resolve,
|
|
@@ -8184,12 +8608,12 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
|
|
|
8184
8608
|
browser: { enabled: false },
|
|
8185
8609
|
},
|
|
8186
8610
|
},
|
|
8187
|
-
|
|
8611
|
+
options ?? {},
|
|
8188
8612
|
)
|
|
8189
8613
|
`);
|
|
8190
8614
|
}
|
|
8191
8615
|
if (app.includes("core")) blocks.push(`
|
|
8192
|
-
${EXPORT_KEYWORD} const appCore = (
|
|
8616
|
+
${EXPORT_KEYWORD} const appCore = (options?: UserConfig): UserConfig =>
|
|
8193
8617
|
mergeConfig(
|
|
8194
8618
|
{
|
|
8195
8619
|
resolve,
|
|
@@ -8203,32 +8627,27 @@ ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
|
|
|
8203
8627
|
browser: { enabled: false },
|
|
8204
8628
|
},
|
|
8205
8629
|
},
|
|
8206
|
-
|
|
8630
|
+
options ?? {},
|
|
8207
8631
|
)
|
|
8208
8632
|
`);
|
|
8209
8633
|
if (app.includes("browser")) blocks.push(facts.showcase === true ? `
|
|
8210
|
-
${
|
|
8211
|
-
|
|
8634
|
+
${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
|
|
8635
|
+
if (config.length > 0) {
|
|
8636
|
+
throw new Error(
|
|
8637
|
+
'[orkestrel-environment-boundary] Browser configuration overrides are not permitted by the generated boundary',
|
|
8638
|
+
)
|
|
8639
|
+
}
|
|
8212
8640
|
return {
|
|
8213
8641
|
resolve,
|
|
8214
8642
|
css: ENVIRONMENT_CSS,
|
|
8215
8643
|
html: environmentHtml(),
|
|
8216
8644
|
plugins: [
|
|
8217
8645
|
restoreHtml(),
|
|
8218
|
-
outputBoundary(
|
|
8646
|
+
outputBoundary('dist/app/browser'),
|
|
8219
8647
|
environmentBoundary('app/browser'),
|
|
8220
8648
|
vue(),
|
|
8221
8649
|
prepareHtml(),
|
|
8222
8650
|
finalizeHtml(),
|
|
8223
|
-
...(showcase
|
|
8224
|
-
? [
|
|
8225
|
-
viteSingleFile({
|
|
8226
|
-
removeViteModuleLoader: true,
|
|
8227
|
-
useRecommendedBuildConfig: true,
|
|
8228
|
-
}),
|
|
8229
|
-
showcaseHtml(),
|
|
8230
|
-
]
|
|
8231
|
-
: []),
|
|
8232
8651
|
],
|
|
8233
8652
|
root: resolveWorkspacePath('app/browser'),
|
|
8234
8653
|
publicDir: false,
|
|
@@ -8239,18 +8658,9 @@ ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
|
|
|
8239
8658
|
},
|
|
8240
8659
|
},
|
|
8241
8660
|
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 }),
|
|
8661
|
+
assetsInlineLimit: 0,
|
|
8252
8662
|
emptyOutDir: true,
|
|
8253
|
-
outDir: resolveWorkspacePath(
|
|
8663
|
+
outDir: resolveWorkspacePath('dist/app/browser'),
|
|
8254
8664
|
rolldownOptions: {
|
|
8255
8665
|
input: resolveWorkspacePath('${APP_MATRIX.browser.entry}'),
|
|
8256
8666
|
},
|
|
@@ -8279,22 +8689,54 @@ ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
|
|
|
8279
8689
|
}
|
|
8280
8690
|
}
|
|
8281
8691
|
|
|
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
8692
|
${EXPORT_KEYWORD} function appShowcase(...config: never[]): UserConfig {
|
|
8292
8693
|
if (config.length > 0) {
|
|
8293
8694
|
throw new Error(
|
|
8294
8695
|
'[orkestrel-environment-boundary] Showcase configuration overrides are not permitted by the generated boundary',
|
|
8295
8696
|
)
|
|
8296
8697
|
}
|
|
8297
|
-
return
|
|
8698
|
+
return {
|
|
8699
|
+
base: './',
|
|
8700
|
+
resolve,
|
|
8701
|
+
css: ENVIRONMENT_CSS,
|
|
8702
|
+
html: environmentHtml(),
|
|
8703
|
+
plugins: [
|
|
8704
|
+
restoreHtml(),
|
|
8705
|
+
outputBoundary('dist/showcase'),
|
|
8706
|
+
environmentBoundary('app/browser'),
|
|
8707
|
+
vue(),
|
|
8708
|
+
prepareHtml(),
|
|
8709
|
+
showcaseHtml(),
|
|
8710
|
+
viteSingleFile({
|
|
8711
|
+
removeViteModuleLoader: true,
|
|
8712
|
+
useRecommendedBuildConfig: true,
|
|
8713
|
+
}),
|
|
8714
|
+
finalizeHtml(),
|
|
8715
|
+
],
|
|
8716
|
+
root: resolveWorkspacePath('app/browser'),
|
|
8717
|
+
publicDir: false,
|
|
8718
|
+
server: {
|
|
8719
|
+
open: '/showcase.html',
|
|
8720
|
+
fs: {
|
|
8721
|
+
strict: true,
|
|
8722
|
+
allow: [...browserServerRoots()],
|
|
8723
|
+
},
|
|
8724
|
+
},
|
|
8725
|
+
build: {
|
|
8726
|
+
assetsInlineLimit: Number.MAX_SAFE_INTEGER,
|
|
8727
|
+
cssMinify: 'lightningcss',
|
|
8728
|
+
emptyOutDir: true,
|
|
8729
|
+
minify: 'oxc',
|
|
8730
|
+
modulePreload: false,
|
|
8731
|
+
outDir: resolveWorkspacePath('dist/showcase'),
|
|
8732
|
+
reportCompressedSize: false,
|
|
8733
|
+
rolldownOptions: {
|
|
8734
|
+
input: resolveWorkspacePath('app/browser/showcase.html'),
|
|
8735
|
+
},
|
|
8736
|
+
sourcemap: false,
|
|
8737
|
+
target: 'esnext',
|
|
8738
|
+
},
|
|
8739
|
+
}
|
|
8298
8740
|
}
|
|
8299
8741
|
` : `
|
|
8300
8742
|
${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
|
|
@@ -8356,7 +8798,7 @@ ${EXPORT_KEYWORD} function appBrowser(...config: never[]): UserConfig {
|
|
|
8356
8798
|
}
|
|
8357
8799
|
`);
|
|
8358
8800
|
if (app.includes("server")) blocks.push(`
|
|
8359
|
-
${EXPORT_KEYWORD} const appServer = (
|
|
8801
|
+
${EXPORT_KEYWORD} const appServer = (options?: UserConfig): UserConfig =>
|
|
8360
8802
|
mergeConfig(
|
|
8361
8803
|
{
|
|
8362
8804
|
resolve,
|
|
@@ -8383,7 +8825,7 @@ ${EXPORT_KEYWORD} const appServer = (config?: UserConfig): UserConfig =>
|
|
|
8383
8825
|
browser: { enabled: false },
|
|
8384
8826
|
},
|
|
8385
8827
|
},
|
|
8386
|
-
|
|
8828
|
+
options ?? {},
|
|
8387
8829
|
)
|
|
8388
8830
|
`);
|
|
8389
8831
|
const renderedTest = renderViteTest(viteProjectRegistrations(src, app, facts), machinery.browser);
|
|
@@ -8618,6 +9060,8 @@ function appTsconfig(environment, hasCore) {
|
|
|
8618
9060
|
const include = TYPESCRIPT_EXTENSIONS.map((extension) => `../../app/${environment}/**/*.${extension}`);
|
|
8619
9061
|
if (environment === "browser") include.push(`../../app/${environment}/**/*.vue`);
|
|
8620
9062
|
if (environment !== "core" && hasCore) include.push(...TYPESCRIPT_EXTENSIONS.map((extension) => `../../app/core/**/*.${extension}`));
|
|
9063
|
+
include.push(...TYPESCRIPT_EXTENSIONS.map((extension) => `../../tests/app/${environment}/**/*.${extension}`));
|
|
9064
|
+
include.push(environment === "browser" ? "../../tests/setupBrowser.ts" : environment === "server" ? "../../tests/setupServer.ts" : "../../tests/setup.ts");
|
|
8621
9065
|
return formatJson({
|
|
8622
9066
|
extends: "../../tsconfig.json",
|
|
8623
9067
|
compilerOptions: {
|
|
@@ -8798,7 +9242,7 @@ export default defineConfig(appShowcase())
|
|
|
8798
9242
|
* factory per environment (AGENTS §5's per-environment centralized-file pattern), so
|
|
8799
9243
|
* every environment gets the same uniform stub shape.
|
|
8800
9244
|
*
|
|
8801
|
-
* @param spec - The
|
|
9245
|
+
* @param spec - The blueprint carrying the declared source environment set.
|
|
8802
9246
|
* @param pascal - The package's PascalCase entity name.
|
|
8803
9247
|
* @returns The `source` group's `Artifact[]`.
|
|
8804
9248
|
*
|
|
@@ -8831,20 +9275,129 @@ function sourceArtifacts(spec, pascal) {
|
|
|
8831
9275
|
* Draft the application source artifacts for every selected app environment.
|
|
8832
9276
|
*
|
|
8833
9277
|
* @param spec - The blueprint carrying the application environment set.
|
|
9278
|
+
* @remarks
|
|
9279
|
+
* Two conditional shapes layer over the per-environment set. The health contract —
|
|
9280
|
+
* record, route constants, guard, and the one unknown-to-typed read — is declared by
|
|
9281
|
+
* `app/server` while the server alone reads it and RELOCATES to `app/core` the moment
|
|
9282
|
+
* the browser reads it too, because a contract two hosts share belongs to neither of
|
|
9283
|
+
* them. The showcase entry pair, its seeder, and its factory appear only for a
|
|
9284
|
+
* blueprint that declares the physical showcase wrapper alongside `app/browser`.
|
|
8834
9285
|
* @returns Complete, runnable app/core, app/browser, and app/server artifacts.
|
|
8835
9286
|
*/
|
|
8836
9287
|
function applicationArtifacts(spec) {
|
|
8837
9288
|
const artifacts = [];
|
|
8838
9289
|
const hasCore = spec.app.includes("core");
|
|
9290
|
+
const hasBrowser = spec.app.includes("browser");
|
|
9291
|
+
const hasBoundary = hasApplicationBoundary(spec);
|
|
9292
|
+
const hasShowcase = hasApplicationShowcase(spec);
|
|
9293
|
+
const showcaseSource = hasBoundary ? "its running server" : "its own configuration";
|
|
8839
9294
|
const nameLiteral = serializeTypeScriptString(spec.name);
|
|
8840
|
-
|
|
8841
|
-
|
|
9295
|
+
const sharedRecord = `
|
|
9296
|
+
/** The application record both hosts read at the health route. */
|
|
9297
|
+
${EXPORT_KEYWORD} interface ApplicationRecord {
|
|
9298
|
+
readonly name: string
|
|
9299
|
+
readonly status: 'ok'
|
|
9300
|
+
}
|
|
9301
|
+
`;
|
|
9302
|
+
const healthConstants = `
|
|
9303
|
+
/** The only HTTP method owned by the application health route. */
|
|
9304
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_METHOD = 'GET'
|
|
9305
|
+
|
|
9306
|
+
/** The only HTTP path owned by the generated application server. */
|
|
9307
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_PATH = '/health'
|
|
9308
|
+
`;
|
|
9309
|
+
if (hasCore) {
|
|
9310
|
+
artifacts.push(fillArtifact("app/core/types.ts", "source", "appCoreTypes", { record: hasBoundary ? sharedRecord : "" }, "core"), fillArtifact("app/core/constants.ts", "source", "appCoreConstants", {
|
|
9311
|
+
nameLiteral,
|
|
9312
|
+
health: hasBoundary ? `${healthConstants}
|
|
9313
|
+
/** Milliseconds allowed for one shared application health read. */
|
|
9314
|
+
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_HEALTH_TIMEOUT = 5_000
|
|
9315
|
+
` : ""
|
|
9316
|
+
}, "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", {
|
|
9317
|
+
validators: hasBoundary ? "export * from './validators.js'\n" : "",
|
|
9318
|
+
handlers: hasBoundary ? "export * from './handlers.js'\n" : ""
|
|
9319
|
+
}, "core"));
|
|
9320
|
+
if (hasBoundary) artifacts.push(fillArtifact("app/core/validators.ts", "source", "appCoreValidators", {}, "core"), fillArtifact("app/core/handlers.ts", "source", "appCoreHandlers", {}, "core"));
|
|
9321
|
+
}
|
|
9322
|
+
if (hasBrowser) {
|
|
8842
9323
|
const nameImport = hasCore ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from './constants.js'";
|
|
8843
9324
|
const nameConstant = hasCore ? "" : `/** The browser-only application name. */
|
|
8844
9325
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
|
|
8845
9326
|
|
|
8846
9327
|
`;
|
|
8847
|
-
artifacts.push(fillArtifact("app/browser/types.ts", "source", "appBrowserTypes", {
|
|
9328
|
+
artifacts.push(fillArtifact("app/browser/types.ts", "source", "appBrowserTypes", { application: hasShowcase && !hasCore ? `
|
|
9329
|
+
/** The identity the root view renders. */
|
|
9330
|
+
${EXPORT_KEYWORD} interface Application {
|
|
9331
|
+
readonly name: string
|
|
9332
|
+
}
|
|
9333
|
+
` : "" }, "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", {
|
|
9334
|
+
nameImport: hasBoundary ? "import { APP_NAME, readApplicationHealth } from '@app/core'" : nameImport,
|
|
9335
|
+
seedImport: hasShowcase ? "import { seedApplication } from './seeders.js'\n" : "",
|
|
9336
|
+
showcase: hasShowcase ? `
|
|
9337
|
+
/**
|
|
9338
|
+
* Mount the showcase over its seeded, inert identity.
|
|
9339
|
+
*
|
|
9340
|
+
* @param target - The browser element or selector that receives the showcase.
|
|
9341
|
+
* @returns The mounted Vue application.
|
|
9342
|
+
*
|
|
9343
|
+
* @remarks
|
|
9344
|
+
* The showcase mounts the same {@link createBrowserApplication} root the shipped entry
|
|
9345
|
+
* mounts, so the two differ in exactly one expression — where the props come from. This
|
|
9346
|
+
* one reads {@link seedApplication}; the application reads ${showcaseSource}.
|
|
9347
|
+
*
|
|
9348
|
+
* @example
|
|
9349
|
+
* \`\`\`ts
|
|
9350
|
+
* import { mountShowcaseApplication } from '@app/browser'
|
|
9351
|
+
*
|
|
9352
|
+
* mountShowcaseApplication('#app')
|
|
9353
|
+
* \`\`\`
|
|
9354
|
+
*/
|
|
9355
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} mountShowcaseApplication(target: string | Element): App<Element> {
|
|
9356
|
+
const seed = seedApplication()
|
|
9357
|
+
const application = createBrowserApplication({ name: seed.name })
|
|
9358
|
+
application.mount(target)
|
|
9359
|
+
return application
|
|
9360
|
+
}
|
|
9361
|
+
` : "",
|
|
9362
|
+
boundary: hasBoundary ? `
|
|
9363
|
+
/**
|
|
9364
|
+
* Mount the application over its real server boundary.
|
|
9365
|
+
*
|
|
9366
|
+
* @param target - The browser element or selector that receives the application.
|
|
9367
|
+
* @returns The mounted Vue application, after one health read settles.
|
|
9368
|
+
*
|
|
9369
|
+
* @remarks
|
|
9370
|
+
* One health read runs before the mount, so the root view renders the identity the
|
|
9371
|
+
* running server reported. An unreachable or off-contract boundary yields \`undefined\`
|
|
9372
|
+
* and the application falls back to its own configuration rather than failing to mount.
|
|
9373
|
+
*
|
|
9374
|
+
* @example
|
|
9375
|
+
* \`\`\`ts
|
|
9376
|
+
* import { mountBrowserApplication } from '@app/browser'
|
|
9377
|
+
*
|
|
9378
|
+
* await mountBrowserApplication('#app')
|
|
9379
|
+
* \`\`\`
|
|
9380
|
+
*/
|
|
9381
|
+
${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} mountBrowserApplication(target: string | Element): Promise<App<Element>> {
|
|
9382
|
+
const seed = (await readApplicationHealth(window.location.origin)) ?? { name: APP_NAME }
|
|
9383
|
+
const application = createBrowserApplication({ name: seed.name })
|
|
9384
|
+
application.mount(target)
|
|
9385
|
+
return application
|
|
9386
|
+
}
|
|
9387
|
+
` : ""
|
|
9388
|
+
}, "browser"), fillArtifact("app/browser/index.ts", "source", "appBrowserIndex", { seeders: hasShowcase ? "export * from './seeders.js'\n" : "" }, "browser"), fillArtifact("app/browser/main.ts", "source", "appBrowserMain", hasBoundary ? {
|
|
9389
|
+
factory: "mountBrowserApplication",
|
|
9390
|
+
mount: `void mountBrowserApplication('#app').catch(() => {
|
|
9391
|
+
console.error('[ERROR] Browser application failed')
|
|
9392
|
+
})`
|
|
9393
|
+
} : {
|
|
9394
|
+
factory: "createBrowserApplication",
|
|
9395
|
+
mount: "createBrowserApplication().mount('#app')"
|
|
9396
|
+
}, "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"));
|
|
9397
|
+
if (hasShowcase) artifacts.push(fillArtifact("app/browser/seeders.ts", "source", "appBrowserSeeders", {
|
|
9398
|
+
applicationImport: hasCore ? "import type { Application } from '@app/core'" : "import type { Application } from './types.js'",
|
|
9399
|
+
nameImport
|
|
9400
|
+
}, "browser"), fillArtifact("app/browser/showcase.ts", "source", "appBrowserShowcase", {}, "browser"), fillArtifact("app/browser/showcase.html", "source", "appBrowserShowcaseHtml", { name: escapeHtmlText(spec.name) }, "browser"));
|
|
8848
9401
|
}
|
|
8849
9402
|
if (spec.app.includes("server")) {
|
|
8850
9403
|
const nameImport = hasCore ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from './constants.js'";
|
|
@@ -8852,7 +9405,19 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
|
|
|
8852
9405
|
${EXPORT_KEYWORD} ${CONST_KEYWORD} APP_NAME = ${nameLiteral}
|
|
8853
9406
|
|
|
8854
9407
|
`;
|
|
8855
|
-
artifacts.push(fillArtifact("app/server/types.ts", "source", "appServerTypes", {
|
|
9408
|
+
artifacts.push(fillArtifact("app/server/types.ts", "source", "appServerTypes", { record: hasBoundary ? "" : `/** The application record returned by the health route. */
|
|
9409
|
+
${EXPORT_KEYWORD} interface ApplicationRecord {
|
|
9410
|
+
readonly name: string
|
|
9411
|
+
readonly status: 'ok'
|
|
9412
|
+
}
|
|
9413
|
+
|
|
9414
|
+
` }, "server"), fillArtifact("app/server/constants.ts", "source", "appServerConstants", {
|
|
9415
|
+
nameConstant,
|
|
9416
|
+
health: hasBoundary ? "" : healthConstants
|
|
9417
|
+
}, "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", {
|
|
9418
|
+
recordImport: hasBoundary ? "import type { ApplicationRecord } from '@app/core'" : "import type { ApplicationRecord } from './types.js'",
|
|
9419
|
+
nameImport
|
|
9420
|
+
}, "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
9421
|
}
|
|
8857
9422
|
return artifacts;
|
|
8858
9423
|
}
|
|
@@ -8925,7 +9490,7 @@ function paritySpecifiers(spec) {
|
|
|
8925
9490
|
function testArtifacts(spec, pascal) {
|
|
8926
9491
|
const hasBrowser = spec.src.includes("browser") || spec.app.includes("browser");
|
|
8927
9492
|
const hasVue = spec.app.includes("browser");
|
|
8928
|
-
const
|
|
9493
|
+
const machinery = viteMachinery(spec.src, spec.app, spec.bin, spec.showcase);
|
|
8929
9494
|
const vuePolicyImport = hasVue ? "\nimport { parse as parseVue } from 'vue/compiler-sfc'" : "";
|
|
8930
9495
|
const workspacePolicyAssertion = hasVue ? `expect(
|
|
8931
9496
|
inspectCodingWorkspace(process.cwd(), (path, content) => {
|
|
@@ -8936,37 +9501,218 @@ function testArtifacts(spec, pascal) {
|
|
|
8936
9501
|
)
|
|
8937
9502
|
}),
|
|
8938
9503
|
).toEqual([])` : "expect(inspectCodingWorkspace(process.cwd())).toEqual([])";
|
|
8939
|
-
const
|
|
9504
|
+
const configNames = [
|
|
9505
|
+
"containedPath",
|
|
9506
|
+
"environmentPathError",
|
|
9507
|
+
"environmentSourceError",
|
|
9508
|
+
"resolveWorkspacePath",
|
|
9509
|
+
"workspacePath"
|
|
9510
|
+
];
|
|
9511
|
+
if (machinery.output) configNames.push("enforceOutputPath");
|
|
9512
|
+
if (hasBrowser) configNames.push("isBrowserExecutable", "resolveBrowser", "resolveManagedBrowser", "resolveSystemBrowser", "SYSTEM_BROWSER_CHANNELS");
|
|
9513
|
+
if (hasVue) configNames.push("hasSecurityPrologue", "HTML_SECURITY_META", "HTML_SECURITY_POLICY");
|
|
9514
|
+
const configImports = [
|
|
9515
|
+
...hasVue ? ["import { readFileSync } from 'node:fs'"] : [],
|
|
9516
|
+
...hasBrowser ? ["import { chromium } from 'playwright'"] : [],
|
|
9517
|
+
"import { describe, expect, it } from 'vitest'",
|
|
9518
|
+
"import {",
|
|
9519
|
+
...configNames.map((name) => `\t${name},`),
|
|
9520
|
+
"} from '../../vite.config.js'"
|
|
9521
|
+
].join("\n");
|
|
9522
|
+
const configCases = [];
|
|
9523
|
+
if (machinery.output) configCases.push(`
|
|
9524
|
+
|
|
9525
|
+
it('contains build output in its exact workspace directory', () => {
|
|
9526
|
+
const expected = resolveWorkspacePath('dist/config-proof')
|
|
9527
|
+
|
|
9528
|
+
expect(() => enforceOutputPath(expected, expected)).not.toThrow()
|
|
9529
|
+
expect(() => enforceOutputPath(resolveWorkspacePath('dist/other'), expected)).toThrow(
|
|
9530
|
+
'exact configured workspace directory',
|
|
9531
|
+
)
|
|
9532
|
+
expect(() =>
|
|
9533
|
+
enforceOutputPath(
|
|
9534
|
+
resolveWorkspacePath('../config-proof'),
|
|
9535
|
+
resolveWorkspacePath('../config-proof'),
|
|
9536
|
+
),
|
|
9537
|
+
).toThrow('remain inside the workspace')
|
|
9538
|
+
})`);
|
|
9539
|
+
if (hasBrowser) configCases.push(`
|
|
8940
9540
|
|
|
8941
9541
|
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
|
-
|
|
9542
|
+
const pinned = chromium.executablePath()
|
|
9543
|
+
const managed = resolveManagedBrowser(pinned)
|
|
9544
|
+
const channel = resolveSystemBrowser(process.platform, process.env)
|
|
9545
|
+
const options = resolveBrowser(pinned, process.platform, process.env)
|
|
9546
|
+
const expected =
|
|
9547
|
+
managed === undefined
|
|
9548
|
+
? channel === undefined
|
|
9549
|
+
? undefined
|
|
9550
|
+
: { launchOptions: { channel } }
|
|
9551
|
+
: managed === pinned
|
|
9552
|
+
? {}
|
|
9553
|
+
: { launchOptions: { executablePath: managed } }
|
|
9554
|
+
const executable = managed === undefined || isBrowserExecutable(managed)
|
|
9555
|
+
const stable =
|
|
9556
|
+
managed !== undefined ||
|
|
9557
|
+
channel === undefined ||
|
|
9558
|
+
SYSTEM_BROWSER_CHANNELS.some((browser) => browser.channel === channel)
|
|
9559
|
+
|
|
9560
|
+
expect(options).toEqual(expected)
|
|
9561
|
+
expect(executable).toBe(true)
|
|
9562
|
+
expect(stable).toBe(true)
|
|
9563
|
+
})`);
|
|
9564
|
+
if (hasVue) configCases.push(`
|
|
9565
|
+
|
|
9566
|
+
it('preserves the generated browser security prologue', () => {
|
|
9567
|
+
const document = readFileSync(resolveWorkspacePath('app/browser/index.html'), 'utf8')
|
|
9568
|
+
|
|
9569
|
+
expect(hasSecurityPrologue(document, HTML_SECURITY_META)).toBe(true)
|
|
9570
|
+
expect(HTML_SECURITY_META).toContain(HTML_SECURITY_POLICY)
|
|
9571
|
+
})`);
|
|
9572
|
+
const artifacts = [
|
|
9573
|
+
fillArtifact("tests/setup.ts", "tests", "setup", {
|
|
9574
|
+
eventImport: spec.app.includes("server") ? "import type { EmitterInterface, EventMap } from '@orkestrel/emitter'\n\n" : "",
|
|
9575
|
+
eventHelper: spec.app.includes("server") ? `
|
|
9576
|
+
/** Wait for one typed event occurrence and return its argument tuple. */
|
|
9577
|
+
${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} waitForEvent<TMap extends EventMap, K extends keyof TMap>(
|
|
9578
|
+
emitter: EmitterInterface<TMap>,
|
|
9579
|
+
event: K,
|
|
9580
|
+
): Promise<TMap[K]> {
|
|
9581
|
+
return new Promise((resolvePromise) => emitter.once(event, (...args) => resolvePromise(args)))
|
|
9582
|
+
}
|
|
9583
|
+
` : ""
|
|
9584
|
+
}),
|
|
9585
|
+
fillArtifact("tests/policy.test.ts", "tests", "policyTest", {
|
|
9586
|
+
vuePolicyImport,
|
|
9587
|
+
workspacePolicyAssertion
|
|
9588
|
+
}),
|
|
9589
|
+
fillArtifact("tests/config/vite.test.ts", "tests", "configTest", {
|
|
9590
|
+
imports: configImports,
|
|
9591
|
+
cases: configCases.join("")
|
|
9592
|
+
})
|
|
9593
|
+
];
|
|
8960
9594
|
if (spec.src.includes("server") || spec.app.includes("server")) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
|
|
8961
9595
|
if (spec.src.includes("browser") || spec.app.includes("browser")) artifacts.push(fillArtifact("tests/setupBrowser.ts", "tests", "setupBrowser", {}, "browser"));
|
|
8962
|
-
|
|
9596
|
+
const hasBoundary = hasApplicationBoundary(spec);
|
|
9597
|
+
const hasShowcase = hasApplicationShowcase(spec);
|
|
9598
|
+
if (spec.app.includes("core")) artifacts.push(fillArtifact("tests/app/core/factories.test.ts", "tests", "appCoreTest", {
|
|
9599
|
+
guardImport: hasBoundary ? " isApplicationRecord,\n" : "",
|
|
9600
|
+
readImport: hasBoundary ? " readApplicationHealth,\n" : "",
|
|
9601
|
+
boundary: hasBoundary ? `
|
|
9602
|
+
|
|
9603
|
+
describe('shared application health boundary', () => {
|
|
9604
|
+
it('accepts the shared record and refuses every off-contract value', () => {
|
|
9605
|
+
expect(isApplicationRecord({ name: APP_NAME, status: 'ok' })).toBe(true)
|
|
9606
|
+
expect(isApplicationRecord({ name: ' ', status: 'ok' })).toBe(true)
|
|
9607
|
+
for (const value of [
|
|
9608
|
+
null,
|
|
9609
|
+
[],
|
|
9610
|
+
'ok',
|
|
9611
|
+
{ name: APP_NAME },
|
|
9612
|
+
{ name: '', status: 'ok' },
|
|
9613
|
+
{ name: 1, status: 'ok' },
|
|
9614
|
+
{ name: APP_NAME, status: 'down' },
|
|
9615
|
+
]) {
|
|
9616
|
+
expect(isApplicationRecord(value)).toBe(false)
|
|
9617
|
+
}
|
|
9618
|
+
const revocable = Proxy.revocable({}, {})
|
|
9619
|
+
revocable.revoke()
|
|
9620
|
+
expect(isApplicationRecord(revocable.proxy)).toBe(false)
|
|
9621
|
+
})
|
|
9622
|
+
|
|
9623
|
+
it('refuses a malformed origin before reaching the network', async () => {
|
|
9624
|
+
expect(await readApplicationHealth('not-an-origin')).toBeUndefined()
|
|
9625
|
+
})
|
|
9626
|
+
})` : ""
|
|
9627
|
+
}, "core"));
|
|
8963
9628
|
if (spec.app.includes("browser")) {
|
|
8964
9629
|
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", {
|
|
9630
|
+
artifacts.push(fillArtifact("tests/app/browser/factories.test.ts", "tests", "appBrowserTest", {
|
|
9631
|
+
browserTestNameImport,
|
|
9632
|
+
showcaseImport: hasShowcase ? " mountShowcaseApplication,\n" : "",
|
|
9633
|
+
entryImport: `${hasShowcase ? " seedApplication,\n" : ""}${hasBoundary ? " mountBrowserApplication,\n" : ""}`,
|
|
9634
|
+
showcase: hasShowcase ? `
|
|
9635
|
+
|
|
9636
|
+
describe('mountShowcaseApplication', () => {
|
|
9637
|
+
it('mounts the shipped root view over one frozen, inert seed', () => {
|
|
9638
|
+
const element = buildElement()
|
|
9639
|
+
const seeded = seedApplication()
|
|
9640
|
+
const application = mountShowcaseApplication(element)
|
|
9641
|
+
try {
|
|
9642
|
+
expect(element.textContent).toContain(seeded.name)
|
|
9643
|
+
expect(seeded).toEqual(seedApplication())
|
|
9644
|
+
expect(seeded).not.toBe(seedApplication())
|
|
9645
|
+
expect(Object.isFrozen(seeded)).toBe(true)
|
|
9646
|
+
} finally {
|
|
9647
|
+
application.unmount()
|
|
9648
|
+
element.remove()
|
|
9649
|
+
}
|
|
9650
|
+
})
|
|
9651
|
+
})` : "",
|
|
9652
|
+
boundary: hasBoundary ? `
|
|
9653
|
+
|
|
9654
|
+
describe('mountBrowserApplication', () => {
|
|
9655
|
+
it('mounts the configured identity when the boundary answers off-contract', async () => {
|
|
9656
|
+
const element = buildElement()
|
|
9657
|
+
const application = await mountBrowserApplication(element)
|
|
9658
|
+
try {
|
|
9659
|
+
expect(element.textContent).toContain(APP_NAME)
|
|
9660
|
+
} finally {
|
|
9661
|
+
application.unmount()
|
|
9662
|
+
element.remove()
|
|
9663
|
+
}
|
|
9664
|
+
})
|
|
9665
|
+
})` : ""
|
|
9666
|
+
}, "browser"));
|
|
8966
9667
|
}
|
|
8967
9668
|
if (spec.app.includes("server")) {
|
|
8968
|
-
const testNameImport =
|
|
8969
|
-
|
|
9669
|
+
const testNameImport = hasBoundary ? `import {
|
|
9670
|
+
APP_HEALTH_METHOD,
|
|
9671
|
+
APP_HEALTH_PATH,
|
|
9672
|
+
APP_NAME,
|
|
9673
|
+
isApplicationRecord,
|
|
9674
|
+
readApplicationHealth,
|
|
9675
|
+
} from '@app/core'` : spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/server'";
|
|
9676
|
+
const serverImport = hasBoundary ? `import {
|
|
9677
|
+
ApplicationServerRunner,
|
|
9678
|
+
createApplicationDispatcher,
|
|
9679
|
+
createApplicationServer,
|
|
9680
|
+
} from '@app/server'` : `import {
|
|
9681
|
+
APP_HEALTH_METHOD,
|
|
9682
|
+
APP_HEALTH_PATH,
|
|
9683
|
+
ApplicationServerRunner,
|
|
9684
|
+
createApplicationDispatcher,
|
|
9685
|
+
createApplicationServer,
|
|
9686
|
+
} from '@app/server'`;
|
|
9687
|
+
artifacts.push(fillArtifact("tests/app/server/ApplicationServer.test.ts", "tests", "appServerTest", {
|
|
9688
|
+
testNameImport,
|
|
9689
|
+
serverImport,
|
|
9690
|
+
boundary: hasBoundary ? `
|
|
9691
|
+
|
|
9692
|
+
describe('shared application boundary', () => {
|
|
9693
|
+
it('answers the shared record and translates it into the shared identity', async () => {
|
|
9694
|
+
const server = createApplicationServer({ server: { host: '127.0.0.1', port: 0 } })
|
|
9695
|
+
try {
|
|
9696
|
+
await server.start()
|
|
9697
|
+
const url = server.url
|
|
9698
|
+
if (url === undefined) throw new Error('Expected a bound application URL')
|
|
9699
|
+
const response = await fetch(\`\${url}\${APP_HEALTH_PATH}\`)
|
|
9700
|
+
const record: unknown = await response.json()
|
|
9701
|
+
|
|
9702
|
+
expect(isApplicationRecord(record)).toBe(true)
|
|
9703
|
+
expect(await readApplicationHealth(url)).toEqual({ name: APP_NAME })
|
|
9704
|
+
} finally {
|
|
9705
|
+
await server.destroy()
|
|
9706
|
+
}
|
|
9707
|
+
})
|
|
9708
|
+
|
|
9709
|
+
it('reads undefined from a released loopback port', async () => {
|
|
9710
|
+
const port = await reserveLoopbackPort()
|
|
9711
|
+
|
|
9712
|
+
expect(await readApplicationHealth(\`http://127.0.0.1:\${port}\`)).toBeUndefined()
|
|
9713
|
+
})
|
|
9714
|
+
})` : ""
|
|
9715
|
+
}, "server"), fillArtifact("tests/app/server/parsers.test.ts", "tests", "appServerParsersTest", {}, "server"));
|
|
8970
9716
|
}
|
|
8971
9717
|
const inlineExplicitInstance = `instance: ${pascal}Interface = new ${pascal}({ id: 'example' })`;
|
|
8972
9718
|
const multilineExplicitInstance = `instance: ${pascal}Interface = new ${pascal}({
|
|
@@ -9010,12 +9756,12 @@ function testArtifacts(spec, pascal) {
|
|
|
9010
9756
|
}
|
|
9011
9757
|
artifacts.push(fillArtifact("tests/setupGuides.ts", "tests", "setupGuides", {
|
|
9012
9758
|
specifiers: paritySpecifiers(spec),
|
|
9013
|
-
walkDirs: [
|
|
9014
|
-
...spec.src.length > 0 ? ["
|
|
9015
|
-
...spec.app.length > 0 ? ["
|
|
9016
|
-
"
|
|
9017
|
-
"
|
|
9018
|
-
]
|
|
9759
|
+
walkDirs: renderStringArray([
|
|
9760
|
+
...spec.src.length > 0 ? ["src"] : [],
|
|
9761
|
+
...spec.app.length > 0 ? ["app"] : [],
|
|
9762
|
+
"guides",
|
|
9763
|
+
"tests"
|
|
9764
|
+
], "", "export const GUIDE_WALK_DIRECTORIES: readonly string[] = Object.freeze(", ")")
|
|
9019
9765
|
}), fillArtifact("tests/guides/src/parity.test.ts", "tests", "parityTest", { name: spec.name }));
|
|
9020
9766
|
return artifacts;
|
|
9021
9767
|
}
|
|
@@ -9064,6 +9810,8 @@ function guideMemberTable(category, members) {
|
|
|
9064
9810
|
*/
|
|
9065
9811
|
function guideUsage(spec, pascal) {
|
|
9066
9812
|
const examples = [];
|
|
9813
|
+
const hasBoundary = hasApplicationBoundary(spec);
|
|
9814
|
+
const hasShowcase = hasApplicationShowcase(spec);
|
|
9067
9815
|
if (spec.src.length > 0) examples.push(`\`\`\`ts
|
|
9068
9816
|
import { create${pascal} } from '@orkestrel/${spec.name}'
|
|
9069
9817
|
|
|
@@ -9080,6 +9828,24 @@ import {
|
|
|
9080
9828
|
${CONST_KEYWORD} name = parseApplicationName(' ${spec.name} ')
|
|
9081
9829
|
${CONST_KEYWORD} application = createApplication(name)
|
|
9082
9830
|
isApplicationError(new ApplicationError('CONFIG', 'invalid')) // true
|
|
9831
|
+
\`\`\``);
|
|
9832
|
+
if (hasBoundary) examples.push(`\`\`\`ts
|
|
9833
|
+
import type { ApplicationRecord } from '@app/core'
|
|
9834
|
+
import {
|
|
9835
|
+
APP_HEALTH_METHOD,
|
|
9836
|
+
APP_HEALTH_PATH,
|
|
9837
|
+
APP_HEALTH_TIMEOUT,
|
|
9838
|
+
isApplicationRecord,
|
|
9839
|
+
readApplicationHealth,
|
|
9840
|
+
} from '@app/core'
|
|
9841
|
+
|
|
9842
|
+
APP_HEALTH_METHOD // 'GET'
|
|
9843
|
+
APP_HEALTH_PATH // '/health'
|
|
9844
|
+
APP_HEALTH_TIMEOUT // 5000
|
|
9845
|
+
${CONST_KEYWORD} healthy: ApplicationRecord = { name: '${spec.name}', status: 'ok' }
|
|
9846
|
+
isApplicationRecord(healthy) // true
|
|
9847
|
+
isApplicationRecord({ name: '${spec.name}', status: 'down' }) // false
|
|
9848
|
+
await readApplicationHealth('http://127.0.0.1:3000') // { name: '${spec.name}' } or undefined
|
|
9083
9849
|
\`\`\``);
|
|
9084
9850
|
if (spec.app.includes("browser")) examples.push(`\`\`\`ts
|
|
9085
9851
|
import {
|
|
@@ -9095,27 +9861,36 @@ ${CONST_KEYWORD} browserOptions = parseBrowserApplicationOptions({
|
|
|
9095
9861
|
${CONST_KEYWORD} browser = createBrowserApplication(browserOptions)
|
|
9096
9862
|
browser.mount('#app')
|
|
9097
9863
|
isBrowserApplicationError(new BrowserApplicationError('CONFIG', 'invalid')) // true
|
|
9864
|
+
\`\`\``);
|
|
9865
|
+
if (hasShowcase) examples.push(`\`\`\`ts
|
|
9866
|
+
import { mountShowcaseApplication, seedApplication } from '@app/browser'
|
|
9867
|
+
|
|
9868
|
+
${CONST_KEYWORD} seed = seedApplication()
|
|
9869
|
+
${CONST_KEYWORD} showcase = mountShowcaseApplication('#app')
|
|
9870
|
+
seed.name // '${spec.name} showcase'
|
|
9871
|
+
showcase.unmount()
|
|
9872
|
+
\`\`\``);
|
|
9873
|
+
if (hasBoundary) examples.push(`\`\`\`ts
|
|
9874
|
+
import { mountBrowserApplication } from '@app/browser'
|
|
9875
|
+
|
|
9876
|
+
${CONST_KEYWORD} application = await mountBrowserApplication('#app')
|
|
9877
|
+
application.unmount()
|
|
9098
9878
|
\`\`\``);
|
|
9099
9879
|
if (spec.app.includes("server")) examples.push(`\`\`\`ts
|
|
9100
|
-
import {
|
|
9101
|
-
import {
|
|
9880
|
+
${hasBoundary ? `import type { ApplicationRecord } from '@app/core'
|
|
9881
|
+
import type { ApplicationState } from '@app/server'
|
|
9882
|
+
import { APP_HEALTH_METHOD, APP_HEALTH_PATH, isApplicationRecord } from '@app/core'` : "import type { ApplicationRecord, ApplicationState } from '@app/server'"}
|
|
9102
9883
|
import {
|
|
9103
|
-
APP_HEALTH_METHOD,
|
|
9884
|
+
${hasBoundary ? "" : ` APP_HEALTH_METHOD,
|
|
9104
9885
|
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,
|
|
9886
|
+
`} APP_HOST_LABEL_PATTERN,
|
|
9112
9887
|
APP_NUMERIC_HOST_PATTERN,
|
|
9113
|
-
ApplicationServer,
|
|
9114
9888
|
ApplicationServerError,
|
|
9115
9889
|
DEFAULT_APP_START_TIMEOUT,
|
|
9116
9890
|
MAX_APP_START_TIMEOUT,
|
|
9891
|
+
createApplicationDispatcher,
|
|
9117
9892
|
createApplicationServer,
|
|
9118
|
-
|
|
9893
|
+
handleApplicationHealth,
|
|
9119
9894
|
isApplicationServerError,
|
|
9120
9895
|
parseApplicationHost,
|
|
9121
9896
|
parseApplicationPort,
|
|
@@ -9127,41 +9902,51 @@ import {
|
|
|
9127
9902
|
${CONST_KEYWORD} host = parseApplicationHost('127.0.0.1')
|
|
9128
9903
|
${CONST_KEYWORD} port = parseApplicationPort('0')
|
|
9129
9904
|
${CONST_KEYWORD} timeout = parseApplicationStartTimeout('5000')
|
|
9130
|
-
${CONST_KEYWORD} options = parseApplicationServerOptions({ host, port, timeout })
|
|
9905
|
+
${CONST_KEYWORD} options = parseApplicationServerOptions({ server: { host, port, timeout } })
|
|
9906
|
+
parseApplicationStartTimeout(String(DEFAULT_APP_START_TIMEOUT)) // 10000
|
|
9907
|
+
MAX_APP_START_TIMEOUT // 300000
|
|
9131
9908
|
APP_HOST_LABEL_PATTERN.test('api') // true
|
|
9132
9909
|
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
|
-
|
|
9910
|
+
${CONST_KEYWORD} state: ApplicationState = { connection: { encrypted: false } }
|
|
9911
|
+
${CONST_KEYWORD} record: ApplicationRecord = { name: '${spec.name}', status: 'ok' }
|
|
9912
|
+
${CONST_KEYWORD} dispatcher = createApplicationDispatcher()
|
|
9913
|
+
try {
|
|
9914
|
+
${CONST_KEYWORD} response = await dispatcher.handle(
|
|
9915
|
+
new Request(\`http://application.test\${APP_HEALTH_PATH}\`, { method: APP_HEALTH_METHOD }),
|
|
9916
|
+
state,
|
|
9917
|
+
)
|
|
9918
|
+
${CONST_KEYWORD} health = handleApplicationHealth()
|
|
9919
|
+
${CONST_KEYWORD} encoded = Response.json(record)
|
|
9920
|
+
${hasBoundary ? `${CONST_KEYWORD} value: unknown = await health.clone().json()
|
|
9921
|
+
isApplicationRecord(value) // true
|
|
9922
|
+
` : ""}if (!response.ok || !health.ok || !encoded.ok) throw new Error('Application health failed')
|
|
9923
|
+
} finally {
|
|
9924
|
+
dispatcher.destroy()
|
|
9925
|
+
}
|
|
9926
|
+
|
|
9927
|
+
${CONST_KEYWORD} failure: unknown = new ApplicationServerError('CONFIG', 'invalid')
|
|
9928
|
+
if (isApplicationServerError(failure)) {
|
|
9929
|
+
reportApplicationServerError(failure) // writes only a stable CONFIG diagnostic
|
|
9930
|
+
}
|
|
9154
9931
|
|
|
9155
9932
|
${CONST_KEYWORD} server = createApplicationServer(options)
|
|
9156
9933
|
${CONST_KEYWORD} controller = new AbortController()
|
|
9157
9934
|
await server.start(controller.signal)
|
|
9158
9935
|
await server.stop()
|
|
9936
|
+
await server.destroy()
|
|
9159
9937
|
\`\`\`
|
|
9160
9938
|
|
|
9161
9939
|
\`\`\`ts
|
|
9162
|
-
import {
|
|
9163
|
-
|
|
9164
|
-
|
|
9940
|
+
import type { ApplicationServerRunnerEventMap, ApplicationServerRunnerOptions } from '@app/server'
|
|
9941
|
+
import { ApplicationServerRunner, createApplicationServer } from '@app/server'
|
|
9942
|
+
|
|
9943
|
+
${CONST_KEYWORD} event: keyof ApplicationServerRunnerEventMap = 'ready'
|
|
9944
|
+
${CONST_KEYWORD} observe: ApplicationServerRunnerOptions = { on: { fail: () => undefined } }
|
|
9945
|
+
${CONST_KEYWORD} runner = new ApplicationServerRunner(
|
|
9946
|
+
createApplicationServer({ server: { port: 0 } }),
|
|
9947
|
+
observe,
|
|
9948
|
+
)
|
|
9949
|
+
runner.emitter.once(event, (url) => console.log(url))
|
|
9165
9950
|
runner.start() // process owns shutdown signals
|
|
9166
9951
|
await runner.stop()
|
|
9167
9952
|
\`\`\`
|
|
@@ -9169,7 +9954,7 @@ await runner.stop()
|
|
|
9169
9954
|
\`\`\`ts
|
|
9170
9955
|
import { startApplicationServer } from '@app/server'
|
|
9171
9956
|
|
|
9172
|
-
${CONST_KEYWORD} processRunner = startApplicationServer({ port: 0 })
|
|
9957
|
+
${CONST_KEYWORD} processRunner = startApplicationServer({ server: { port: 0 } })
|
|
9173
9958
|
await processRunner.stop()
|
|
9174
9959
|
\`\`\``);
|
|
9175
9960
|
return examples.join("\n\n");
|
|
@@ -9192,15 +9977,23 @@ ${alignTable([
|
|
|
9192
9977
|
"Method",
|
|
9193
9978
|
"Returns",
|
|
9194
9979
|
"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
9980
|
], [
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9981
|
+
[
|
|
9982
|
+
"`start`",
|
|
9983
|
+
"`Promise<void>`",
|
|
9984
|
+
"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."
|
|
9985
|
+
],
|
|
9986
|
+
[
|
|
9987
|
+
"`stop`",
|
|
9988
|
+
"`Promise<void>`",
|
|
9989
|
+
"Drain and stop the installed server; repeated calls while stopped are safe. Rejects with `ApplicationServerError` code `LIFECYCLE` when closing fails."
|
|
9990
|
+
],
|
|
9991
|
+
[
|
|
9992
|
+
"`destroy`",
|
|
9993
|
+
"`Promise<void>`",
|
|
9994
|
+
"Perform terminal idempotent teardown through the installed server lifecycle, then destroy its owned dispatcher. Rejects with `ApplicationServerError` code `LIFECYCLE` when server teardown fails."
|
|
9995
|
+
]
|
|
9996
|
+
])}
|
|
9204
9997
|
|
|
9205
9998
|
#### \`ApplicationServerRunnerInterface\`
|
|
9206
9999
|
|
|
@@ -9211,17 +10004,18 @@ ${alignTable([
|
|
|
9211
10004
|
], [[
|
|
9212
10005
|
"`start`",
|
|
9213
10006
|
"`void`",
|
|
9214
|
-
"Register one
|
|
10007
|
+
"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
10008
|
], [
|
|
9216
10009
|
"`stop`",
|
|
9217
10010
|
"`Promise<void>`",
|
|
9218
|
-
"
|
|
10011
|
+
"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
10012
|
]])}
|
|
9220
10013
|
|
|
9221
|
-
The
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
\`
|
|
10014
|
+
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.
|
|
10015
|
+
|
|
10016
|
+
The application server constructor validates grouped direct options plus \`APP_HOST\`, \`APP_PORT\`, and
|
|
10017
|
+
\`APP_START_TIMEOUT\` before binding. Direct options must be an exact plain own-key data record containing only a
|
|
10018
|
+
\`server\` record with \`host\`, \`port\`, and/or \`timeout\`; inherited properties, accessors, symbols, instances, proxies that
|
|
9225
10019
|
throw during reflection, and unknown keys fail closed. Invalid values throw
|
|
9226
10020
|
\`ApplicationServerError\` code \`CONFIG\`; the default host is loopback and port \`0\` is
|
|
9227
10021
|
supported for collision-free ephemeral allocation. Startup defaults to 10 seconds and accepts
|
|
@@ -9229,10 +10023,15 @@ only integer timeouts from 1 through 300,000 milliseconds. Lifecycle failures us
|
|
|
9229
10023
|
\`LIFECYCLE\`; both may carry \`context.cause\` or \`context.value\`. Narrow caught values with
|
|
9230
10024
|
\`isApplicationServerError\` before reading either field.
|
|
9231
10025
|
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
10026
|
+
Before binding, \`url\` is \`undefined\`; after a successful start it reflects the real bound port,
|
|
10027
|
+
and it returns to \`undefined\` after stop or destroy. \`ApplicationState\` extends middleware's
|
|
10028
|
+
\`IdentifierState\` and adds only its \`connection\` property; there is no redundant \`listening\` member.
|
|
10029
|
+
|
|
10030
|
+
Each \`createApplicationDispatcher()\` call returns a fresh dispatcher that owns exactly \`GET /health\`
|
|
10031
|
+
and serializes the shared \`ApplicationRecord\` shape \`{ name: APP_NAME, status: 'ok' }\` as JSON. The
|
|
10032
|
+
server composes \`createBoundary()\`, \`createSecurity()\`, then \`createDeadline({ ms: timeout })\`
|
|
10033
|
+
around that owned dispatcher; standalone callers destroy theirs after use. Every other path returns
|
|
10034
|
+
\`404\`, and every unsupported method returns \`405\` with \`Allow: GET\`.`;
|
|
9236
10035
|
}
|
|
9237
10036
|
/**
|
|
9238
10037
|
* Build links to every generated source and application test file.
|
|
@@ -9242,7 +10041,7 @@ method returns deterministic plain-text \`405 Method Not Allowed\` with \`Allow:
|
|
|
9242
10041
|
* @returns A newline-separated Markdown test inventory.
|
|
9243
10042
|
*/
|
|
9244
10043
|
function guideTests(spec, pascal) {
|
|
9245
|
-
const tests = ["- [`tests/policy.test.ts`](../../tests/policy.test.ts) — filename placement and
|
|
10044
|
+
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
10045
|
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
10046
|
if (spec.app.includes("core")) tests.push("- [`tests/app/core/factories.test.ts`](../../tests/app/core/factories.test.ts) — host-independent identity behavior.");
|
|
9248
10047
|
if (spec.app.includes("browser")) tests.push("- [`tests/app/browser/factories.test.ts`](../../tests/app/browser/factories.test.ts) — real-browser mount and cleanup.");
|
|
@@ -9922,6 +10721,6 @@ function createBlueprint(data) {
|
|
|
9922
10721
|
return candidate;
|
|
9923
10722
|
}
|
|
9924
10723
|
//#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 };
|
|
10724
|
+
export { APP_BROWSER_DEV_DEPENDENCIES, APP_DEV_DEPENDENCIES, APP_MATRIX, APP_SERVER_DEV_DEPENDENCIES, 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, 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, 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
10725
|
|
|
9927
10726
|
//# sourceMappingURL=index.js.map
|