@orkestrel/scaffold 0.0.20 → 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.
@@ -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.20";
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.61.0",
239
- oxlint: "^1.76.0",
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
- members.push(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("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_MAX_CONNECTIONS", "constant", "The simultaneous connection limit.", "server"), member("APP_MAX_HEADERS", "constant", "The request-header count limit.", "server"), member("APP_HEADERS_TIMEOUT", "constant", "The request-header timeout.", "server"), member("APP_REQUEST_TIMEOUT", "constant", "The complete-request timeout.", "server"), member("APP_KEEP_ALIVE_TIMEOUT", "constant", "The idle keep-alive timeout.", "server"), member("APP_MAX_REQUESTS_PER_SOCKET", "constant", "The keep-alive request limit.", "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"), member("APP_HEALTH_METHOD", "constant", "The owned health request method.", "server"), member("APP_HEALTH_PATH", "constant", "The owned health request path.", "server"), member("ApplicationServer", "entity", "The Node HTTP 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("handleApplicationRequest", "handler", "Handle an application HTTP request.", "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"));
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
- try {
2968
- return value instanceof ApplicationError
2969
- } catch {
2970
- return false
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 { MAX_APPLICATION_NAME_INPUT_LENGTH, MAX_APPLICATION_NAME_LENGTH } from './constants.js'
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.length === 0 || name.length > MAX_APPLICATION_NAME_LENGTH) {
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
- try {
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.length === 0 || name.length > MAX_BROWSER_APPLICATION_NAME_LENGTH) {
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
- appBrowserFactories: Object.freeze({
3224
- id: "appBrowserFactories",
3225
- name: "appBrowserFactories",
3226
- summary: "The Vue browser application factory.",
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 { createBrowserApplication } from './index.js'
3511
+ content: `import { mountShowcaseApplication } from './index.js'
3278
3512
 
3279
- createBrowserApplication().mount('#app')
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
- content: `/** A rejected application server boundary. */
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 host?: string
3360
- readonly port?: number
3361
- readonly timeout?: number
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 listening: boolean
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
- try {
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.length === 0 ||
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
- if (typeof value !== 'string' && typeof value !== 'number') {
3548
- throw new ApplicationServerError('CONFIG', 'APP_PORT must be an integer from 0 through 65535', {
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 text = typeof value === 'string' ? value.trim() : String(value)
3558
- const port = APP_PORT_PATTERN.test(text) ? Number(text) : Number.NaN
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
- if (typeof value !== 'string' && typeof value !== 'number') {
3577
- throw new ApplicationServerError(
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 text = typeof value === 'string' ? value.trim() : String(value)
3591
- const timeout = APP_PORT_PATTERN.test(text) ? Number(text) : Number.NaN
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 !== 'host' && key !== 'port' && key !== 'timeout')
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 hostDescriptor = Reflect.getOwnPropertyDescriptor(value, 'host')
3632
- const portDescriptor = Reflect.getOwnPropertyDescriptor(value, 'port')
3633
- const timeoutDescriptor = Reflect.getOwnPropertyDescriptor(value, 'timeout')
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
- (keys.includes('host') &&
3932
+ (serverKeys.includes('host') &&
3636
3933
  (hostDescriptor === undefined || !Reflect.has(hostDescriptor, 'value'))) ||
3637
- (keys.includes('port') &&
3934
+ (serverKeys.includes('port') &&
3638
3935
  (portDescriptor === undefined || !Reflect.has(portDescriptor, 'value'))) ||
3639
- (keys.includes('timeout') &&
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 options must use data properties',
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
- ...(host === undefined ? {} : { host: parseApplicationHost(host) }),
3653
- ...(port === undefined ? {} : { port: parseApplicationPort(port) }),
3654
- ...(timeout === undefined ? {} : { timeout: parseApplicationStartTimeout(timeout) }),
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 HTTP request handler.",
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: `import type { IncomingMessage, ServerResponse } from 'node:http'
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
- * Respond to the application health endpoint and reject every other route.
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 { Server } from 'node:http'
3743
- import type { ApplicationServerInterface, ApplicationServerOptions } from './types.js'
3744
- import { once } from 'node:events'
3745
- import { createServer } from 'node:http'
3746
- import { promisify } from 'node:util'
3747
- import {
3748
- APP_HEADERS_TIMEOUT,
3749
- APP_KEEP_ALIVE_TIMEOUT,
3750
- APP_MAX_CONNECTIONS,
3751
- APP_MAX_HEADERS,
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 Node HTTP application server. */
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 #requestedPort: number
3768
- readonly #timeout: number
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
- this.host = parseApplicationHost(parsed.host === undefined ? process.env.APP_HOST : parsed.host)
3778
- this.#requestedPort = parseApplicationPort(
3779
- parsed.port === undefined ? process.env.APP_PORT : parsed.port,
4076
+ const server = parsed.server
4077
+ const host = parseApplicationHost(
4078
+ server?.host === undefined ? process.env.APP_HOST : server.host,
3780
4079
  )
3781
- this.#port = this.#requestedPort
3782
- this.#timeout = parseApplicationStartTimeout(
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
- this.#server = createServer(handleApplicationRequest)
3786
- this.#server.maxConnections = APP_MAX_CONNECTIONS
3787
- this.#server.maxHeadersCount = APP_MAX_HEADERS
3788
- this.#server.headersTimeout = APP_HEADERS_TIMEOUT
3789
- this.#server.requestTimeout = APP_REQUEST_TIMEOUT
3790
- this.#server.keepAliveTimeout = APP_KEEP_ALIVE_TIMEOUT
3791
- this.#server.maxRequestsPerSocket = APP_MAX_REQUESTS_PER_SOCKET
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
- get port(): number {
3795
- return this.#port
4102
+ static #state(connection: ConnectionInfo): ApplicationState {
4103
+ return { connection }
3796
4104
  }
3797
4105
 
3798
- get listening(): boolean {
3799
- return this.#server.listening
4106
+ get port(): number | undefined {
4107
+ return this.#server.port
3800
4108
  }
3801
4109
 
3802
- get url(): string {
3803
- const hostname = this.host.includes(':') ? \`[\${this.host}]\` : this.host
3804
- return \`http://\${hostname}:\${this.port}\`
4110
+ get status(): ApplicationServerInterface['status'] {
4111
+ return this.#server.status
3805
4112
  }
3806
4113
 
3807
- start(signal?: AbortSignal): Promise<void> {
3808
- const controller = new AbortController()
3809
- this.#starts.add(controller)
3810
- const queued = this.#queue(this.#start.bind(this, controller, signal))
3811
- void queued.then(this.#settle.bind(this, controller), this.#settle.bind(this, controller))
3812
- return queued
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
- stop(): Promise<void> {
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
- if (signal?.aborted === true) {
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
- if (this.#stopped.has(controller)) return
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 #stop(): Promise<void> {
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
- if (signal !== undefined && relay !== undefined) {
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 start application server', {
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
- #abort(controller: AbortController, signal: AbortSignal): void {
3891
- controller.abort(signal.reason)
3892
- }
3893
-
3894
- #expire(controller: AbortController): void {
3895
- controller.abort(new Error(\`Application server startup exceeded \${this.#timeout} milliseconds\`))
3896
- }
3897
-
3898
- #settle(controller: AbortController): void {
3899
- this.#starts.delete(controller)
3900
- }
3901
-
3902
- async #close(): Promise<void> {
4141
+ async destroy(): Promise<void> {
3903
4142
  try {
3904
- const closed = promisify(this.#server.close.bind(this.#server))()
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 stop application server', {
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 host and port overrides.
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 host and port overrides.
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
- content: `import type {
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
- ApplicationServerOptions,
4226
+ ApplicationServerRunnerEventMap,
3984
4227
  ApplicationServerRunnerInterface,
4228
+ ApplicationServerRunnerOptions,
3985
4229
  } from './types.js'
3986
- import { ApplicationServer } from './ApplicationServer.js'
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 #signal: () => void
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: ApplicationServerOptions = {}) {
3997
- this.#server = new ApplicationServer(options)
3998
- this.#signal = this.#shutdown.bind(this)
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
- process.once('SIGINT', this.#signal)
4006
- process.once('SIGTERM', this.#signal)
4007
- void this.#server.start().catch(this.#fail.bind(this, generation))
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.#generation += 1
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
- return this.#server.stop()
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
- reportApplicationServerError(error)
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
- const generation = this.#generation
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.#signal)
4030
- process.off('SIGTERM', this.#signal)
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` recorder helper — no placeholders.",
4397
+ summary: "The generated-minimal `tests/setup.ts` shared test helpers.",
4071
4398
  category: "tests",
4072
- placeholders: Object.freeze([]),
4073
- content: `// ── Call recorder (a real callback, not a mock) ──────────────────────────────
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
- Object.freeze({
4128
- name: "browserPolicySpecifier",
4129
- description: "The optional real Chromium filesystem-probe import."
4130
- }),
4131
- Object.freeze({
4132
- name: "browserPolicyImport",
4133
- description: "The optional real Chromium package import."
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'{{browserPolicyImport}}{{vuePolicyImport}}
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
- }){{browserPolicyTest}}
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
- /** Reserve and release a real loopback port for an immediate child-process bind. */
4222
- ${EXPORT_KEYWORD} async ${FUNCTION_KEYWORD} reserveLoopbackPort(): Promise<number> {
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(0, '127.0.0.1')
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 === 'string') output.push(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([Object.freeze({
4496
- name: "browserTestNameImport",
4497
- description: "The layer-correct browser APP_NAME test import."
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 loopback application server lifecycle test.",
4998
+ summary: "The real dispatcher, server-substrate, and runner integration test.",
4597
4999
  category: "tests",
4598
- placeholders: Object.freeze([Object.freeze({
4599
- name: "testNameImport",
4600
- description: "The layer-correct APP_NAME test import."
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
- import {
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('ApplicationServer', () => {
4629
- it('serves a real loopback request and tolerates repeated lifecycle calls', async () => {
4630
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
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
- await Promise.all([server.start(), server.start(), server.start()])
4633
- await server.start()
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
- await Promise.all([server.stop(), server.stop(), server.stop()])
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('serves concurrent loopback requests without cross-request state', async () => {
4666
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
5048
+ it('distinguishes a wrong method from a missing route', async () => {
5049
+ const dispatcher = createApplicationDispatcher()
4667
5050
  try {
4668
- await server.start()
4669
- const responses = await Promise.all(
4670
- Array.from({ length: 8 }, async () => {
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
- it('parses the real APP_HOST, APP_PORT, and APP_START_TIMEOUT environment boundary and restores it', () => {
4709
- const previousHost = process.env.APP_HOST
4710
- const previousPort = process.env.APP_PORT
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
- if (previousHost === undefined) delete process.env.APP_HOST
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
- it('rejects an aborted startup promptly, cleans up, and remains restartable', async () => {
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
- const started = Date.now()
4745
- await expect(server.start(controller.signal)).rejects.toMatchObject({ code: 'LIFECYCLE' })
4746
- expect(Date.now() - started).toBeLessThan(5_000)
4747
- expect(server.listening).toBe(false)
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((await fetch(server.url)).status).toBe(200)
4752
- } finally {
4753
- await server.stop()
4754
- }
4755
- })
4756
-
4757
- it('contains a revoked startup signal without listening or leaking transition state', async () => {
4758
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4759
- const revocable = Proxy.revocable(new AbortController().signal, {})
4760
- revocable.revoke()
4761
- try {
4762
- await expect(Reflect.apply(server.start, server, [revocable.proxy])).rejects.toMatchObject({
4763
- code: 'LIFECYCLE',
4764
- })
4765
- expect(server.listening).toBe(false)
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
- await Promise.all([server.start(), server.start()])
4795
- expect((await fetch(server.url)).status).toBe(200)
4796
- } finally {
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 previousPort = server.port
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
- occupant.listen(previousPort, server.host)
4810
- await once(occupant, 'listening')
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.stop()
4817
- await stopNodeServer(occupant)
5106
+ await server.destroy()
4818
5107
  }
4819
5108
  })
4820
5109
 
4821
- it('honors the latest requested state across opposing concurrent transitions', async () => {
4822
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4823
- try {
4824
- const firstStart = server.start()
4825
- const stopping = server.stop()
4826
- const latestStart = server.start()
4827
- await Promise.all([firstStart, stopping, latestStart])
4828
-
4829
- expect(server.listening).toBe(true)
4830
- expect((await fetch(server.url)).status).toBe(200)
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
- it('forces a hostile partial-header connection closed during stop', async () => {
4837
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
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
- await server.start()
4840
- const socket = connect({ host: server.host, port: server.port })
4841
- try {
4842
- await once(socket, 'connect')
4843
- socket.write('GET / HTTP/1.1\\r\\nHost: localhost')
4844
- const closed = waitForSocketClose(socket)
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
- await server.stop()
4847
- await closed
4848
- expect(socket.destroyed).toBe(true)
4849
- } finally {
4850
- socket.destroy()
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
- await server.stop()
5149
+ process.exitCode = code
5150
+ await runner.stop()
4854
5151
  }
4855
5152
  })
4856
5153
 
4857
- it('bounds simultaneous idle connections and recovers after capacity is released', async () => {
4858
- const server = createApplicationServer({ host: '127.0.0.1', port: 0 })
4859
- const sockets = []
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
- await server.start()
4862
- for (let index = 0; index < APP_MAX_CONNECTIONS; index += 1) {
4863
- const socket = connect({ host: server.host, port: server.port })
4864
- await once(socket, 'connect')
4865
- socket.write('GET / HTTP/1.1\\r\\nHost: localhost')
4866
- sockets.push(socket)
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
- const closed = waitForSocketClose(overflow)
4871
- await once(overflow, 'connect')
4872
- await closed
4873
- expect(overflow.destroyed).toBe(true)
5170
+ expect(released.listening).toBe(true)
4874
5171
  } finally {
4875
- overflow.destroy()
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
- for (const socket of sockets) socket.destroy()
4886
- await server.stop()
5175
+ await runner.stop()
4887
5176
  }
4888
5177
  })
4889
5178
 
4890
- it('starts the built executable, serves loopback traffic, terminates on SIGTERM, and releases its port', async () => {
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
- const response = await waitForApplicationResponse(application, port)
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 = createServer()
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('exits nonzero on a real executable port collision without disturbing the owner', async () => {
5214
+ it('never announces readiness when a real bind fails', async () => {
4923
5215
  const port = await reserveLoopbackPort()
4924
- const owner = createServer()
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('redacts malformed environment values from executable diagnostics', async () => {
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 = createServer()
4998
- owner.listen(port, '127.0.0.1')
4999
- await once(owner, 'listening')
5000
- const runner = new ApplicationServerRunner({ host: '127.0.0.1', port })
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
- await Promise.resolve()
5004
- const stopping = runner.stop()
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 response = await waitForLoopbackResponse(port)
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.each([null, 42, [], { port: [42] }, { timeout: 0 }])(
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
- { [Symbol('host')]: '0.0.0.0' },
5110
- Object.defineProperty({}, 'host', { get: () => '0.0.0.0' }),
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 = [policyViteProject(), guidesViteProject()];
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
- ${EXPORT_KEYWORD} ${CONST_KEYWORD} HTML_SECURITY_PREFIX =
6884
- '<!doctype html>\\n<html lang="en">\\n\\t<head>\\n\\t\\t' + HTML_SECURITY_META + '\\n'
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(environmentKeys: ReadonlySet<string>, html: string): string {
6887
- if (!html.replaceAll('\\r\\n', '\\n').startsWith(HTML_SECURITY_PREFIX)) {
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 isBrowserHtmlEntry(filename: string): boolean {
7273
+ ${needsShowcase ? `${EXPORT_KEYWORD} function isShowcaseHtmlEntry(filename: string): boolean {
6922
7274
  return (
6923
- physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
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
- if (!isBrowserHtmlEntry(context.filename)) return undefined
6944
- return maskIgnoredHtml(environmentKeys, html)
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
- if (!isBrowserHtmlEntry(context.filename)) return undefined
6969
- if (!html.includes(HTML_SECURITY_META)) {
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} ${CONST_KEYWORD} SHOWCASE_SECURITY_POLICY =
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 (!isBrowserHtmlEntry(context.filename)) return undefined
6990
- if (!html.includes(HTML_SECURITY_META)) {
6991
- throw new Error('[orkestrel-showcase] Browser HTML must retain its security policy')
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
- const stamp = new Date().toISOString()
6994
- return html.replace(
6995
- HTML_SECURITY_META,
6996
- HTML_SECURITY_META.replace(HTML_SECURITY_POLICY, SHOWCASE_SECURITY_POLICY) +
6997
- \`\\n\\t\\t<meta name="build-id" content="\${stamp}" />\`,
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- ...(config?.test?.browser?.enabled === false
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- ...(config?.test?.browser?.enabled === false
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
- config ?? {},
8394
+ options ?? {},
7971
8395
  ),
7972
8396
  )
7973
8397
  `;
7974
8398
  const serverBlock = `
7975
- ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- ...(config?.test?.browser?.enabled === false
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
- config ?? {},
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
8611
+ options ?? {},
8188
8612
  )
8189
8613
  `);
8190
8614
  }
8191
8615
  if (app.includes("core")) blocks.push(`
8192
- ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
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
- config ?? {},
8630
+ options ?? {},
8207
8631
  )
8208
8632
  `);
8209
8633
  if (app.includes("browser")) blocks.push(facts.showcase === true ? `
8210
- ${FUNCTION_KEYWORD} applicationBrowser(showcase: boolean): UserConfig {
8211
- const output = showcase ? 'dist/showcase' : 'dist/app/browser'
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(output),
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
- ...(showcase
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(output),
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 applicationBrowser(true)
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 = (config?: UserConfig): UserConfig =>
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
- config ?? {},
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 `Blueprint` to derive source stubs from.
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
- if (hasCore) artifacts.push(fillArtifact("app/core/types.ts", "source", "appCoreTypes", {}, "core"), fillArtifact("app/core/constants.ts", "source", "appCoreConstants", { nameLiteral }, "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", {}, "core"));
8841
- if (spec.app.includes("browser")) {
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", {}, "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", { nameImport }, "browser"), fillArtifact("app/browser/index.ts", "source", "appBrowserIndex", {}, "browser"), fillArtifact("app/browser/main.ts", "source", "appBrowserMain", {}, "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"));
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", {}, "server"), fillArtifact("app/server/constants.ts", "source", "appServerConstants", { nameConstant }, "server"), fillArtifact("app/server/errors.ts", "source", "appServerErrors", {}, "server"), fillArtifact("app/server/parsers.ts", "source", "appServerParsers", {}, "server"), fillArtifact("app/server/handlers.ts", "source", "appServerHandlers", { nameImport }, "server"), fillArtifact("app/server/ApplicationServer.ts", "source", "appServerEntity", {}, "server"), fillArtifact("app/server/factories.ts", "source", "appServerFactories", {}, "server"), fillArtifact("app/server/ApplicationServerRunner.ts", "source", "appServerRunner", {}, "server"), fillArtifact("app/server/index.ts", "source", "appServerIndex", {}, "server"), fillArtifact("app/server/main.ts", "source", "appServerMain", {}, "server"));
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 browserPolicyImport = hasBrowser ? "\nimport { chromium } from 'playwright'\nimport { isBrowserExecutable, resolveBrowser, SYSTEM_BROWSER_CHANNELS } from '../vite.config.js'" : "";
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 browserPolicyTest = hasBrowser ? `
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 options = resolveBrowser(chromium.executablePath(), process.platform, process.env)
8943
- let valid = options === undefined
8944
- if (options !== undefined) {
8945
- const channel = options.launchOptions?.channel
8946
- valid =
8947
- channel === undefined
8948
- ? isBrowserExecutable(options.launchOptions?.executablePath ?? chromium.executablePath())
8949
- : SYSTEM_BROWSER_CHANNELS.some((browser) => browser.channel === channel)
8950
- }
8951
- expect(valid).toBe(true)
8952
- })` : "";
8953
- const artifacts = [fillArtifact("tests/setup.ts", "tests", "setup", {}), fillArtifact("tests/policy.test.ts", "tests", "policyTest", {
8954
- browserPolicySpecifier: "",
8955
- browserPolicyImport,
8956
- browserPolicyTest,
8957
- vuePolicyImport,
8958
- workspacePolicyAssertion
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
- if (spec.app.includes("core")) artifacts.push(fillArtifact("tests/app/core/factories.test.ts", "tests", "appCoreTest", {}, "core"));
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", { browserTestNameImport }, "browser"));
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 = spec.app.includes("core") ? "import { APP_NAME } from '@app/core'" : "import { APP_NAME } from '@app/server'";
8969
- artifacts.push(fillArtifact("tests/app/server/ApplicationServer.test.ts", "tests", "appServerTest", { testNameImport }, "server"), fillArtifact("tests/app/server/parsers.test.ts", "tests", "appServerParsersTest", {}, "server"));
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 ? ["'src'"] : [],
9015
- ...spec.app.length > 0 ? ["'app'"] : [],
9016
- "'guides'",
9017
- "'tests'"
9018
- ].map((directory) => `\t${directory},`).join("\n")
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 { once } from 'node:events'
9101
- import { createServer } from 'node:http'
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
- APP_HEADERS_TIMEOUT,
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
- handleApplicationRequest,
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
- APP_HEALTH_METHOD // 'GET'
9134
- APP_HEALTH_PATH // '/'
9135
- APP_MAX_CONNECTIONS // 16
9136
- APP_MAX_HEADERS // 100
9137
- APP_HEADERS_TIMEOUT // 10000
9138
- APP_REQUEST_TIMEOUT // 30000
9139
- APP_KEEP_ALIVE_TIMEOUT // 5000
9140
- APP_MAX_REQUESTS_PER_SOCKET // 100
9141
- DEFAULT_APP_START_TIMEOUT // 10000
9142
- MAX_APP_START_TIMEOUT // 300000
9143
- ${CONST_KEYWORD} handlerServer = createServer(handleApplicationRequest)
9144
- handlerServer.listen(0, host)
9145
- await once(handlerServer, 'listening')
9146
- ${CONST_KEYWORD} handlerClosed = once(handlerServer, 'close')
9147
- handlerServer.close()
9148
- await handlerClosed
9149
-
9150
- ${CONST_KEYWORD} error = new ApplicationServerError('CONFIG', 'invalid')
9151
- isApplicationServerError(error) // true
9152
- reportApplicationServerError(error) // writes only a stable CONFIG diagnostic
9153
- new ApplicationServer(options) // stopped entity
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 { ApplicationServerRunner } from '@app/server'
9163
-
9164
- ${CONST_KEYWORD} runner = new ApplicationServerRunner({ port: 0 })
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
- "`stop`",
9201
- "`Promise<void>`",
9202
- "Cancel every pending start before its queued stop, force active and idle connections closed, and repeat safely when already stopped. Rejects with `ApplicationServerError` code `LIFECYCLE` when closing fails."
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 idempotent set of SIGINT/SIGTERM cleanup listeners, start the server, and translate asynchronous startup failures into a non-zero process exit code."
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
- "Release both process listeners before stopping the server; repeated calls are safe and lifecycle failures reject."
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 constructor validates direct options plus \`APP_HOST\`, \`APP_PORT\`, and
9222
- \`APP_START_TIMEOUT\` before allocating
9223
- a listener. Direct options must be an exact plain own-key data record containing only
9224
- \`host\`, \`port\`, and/or \`timeout\`; inherited properties, accessors, symbols, instances, proxies that
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
- The generated server owns exactly \`GET /\`. It serializes
9233
- \`{ name: APP_NAME, status: 'ok' }\` as JSON with \`cache-control: no-store\`;
9234
- every other path returns deterministic plain-text \`404 Not Found\`, and every unsupported
9235
- method returns deterministic plain-text \`405 Method Not Allowed\` with \`Allow: GET\`.`;
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 real browser capability probing."];
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