@orkestrel/scaffold 0.0.7 → 0.0.8

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.
@@ -77,6 +77,8 @@ var SRC_MATRIX = Object.freeze({
77
77
  formats: Object.freeze(["es", "cjs"])
78
78
  })
79
79
  });
80
+ /** The computed configuration files required by a workspace-owned executable. */
81
+ var BIN_CONFIGS = Object.freeze(["configs/src/vite.bin.config.ts", "configs/src/tsconfig.bin.json"]);
80
82
  /**
81
83
  * The per-environment application matrix: thin config artifacts, Vitest project
82
84
  * label, and executable entry where the environment produces a runtime bundle.
@@ -106,7 +108,7 @@ var APP_MATRIX = Object.freeze({
106
108
  * `scripts/codex.sh` / `scripts/ollama.sh`), the repository coding-law policy module,
107
109
  * the line's seven byte-identical root dotfiles, and the two guides-grouped
108
110
  * mirror candidates: the line-wide dev-tooling guide
109
- * (`guides/src/guide.md`) and the scaffold engine's own self-guide
111
+ * (`guides/src/guide.md`) and the scaffold bin's own self-guide
110
112
  * (`guides/src/scaffold.md`). `stageHost` vendors both; each plan carries the
111
113
  * subset selected by `selectHostPaths`, omitting the target blueprint's own
112
114
  * guide.
@@ -139,6 +141,8 @@ var HOST_PATHS = Object.freeze([
139
141
  "guides/src/guide.md",
140
142
  "guides/src/scaffold.md"
141
143
  ]);
144
+ /** The consumer-owned live-service provisioner expected only by service workspaces. */
145
+ var SERVICE_SCRIPT_PATH = "scripts/service.sh";
142
146
  /** The package-name RegExp — lowercase alphanumeric-with-hyphens, letter-first. */
143
147
  var NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
144
148
  /** Maximum bare workspace name length beneath the generated `@orkestrel/` scope. */
@@ -221,11 +225,11 @@ var DEFAULT_VERSION = "0.0.1";
221
225
  /** The `engines.node` range the `blueprint` builder fills. */
222
226
  var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
223
227
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
224
- var SCAFFOLD_RANGE = "^0.0.7";
228
+ var SCAFFOLD_RANGE = "^0.0.8";
225
229
  /** Tooling versions shared by scaffold and every generated workspace. */
226
230
  var BASE_DEV_DEPENDENCIES = Object.freeze({
227
231
  "@microsoft/api-extractor": "^7.58.12",
228
- "@orkestrel/guide": "^0.0.5",
232
+ "@orkestrel/guide": "^0.0.7",
229
233
  "@orkestrel/scaffold": SCAFFOLD_RANGE,
230
234
  "@types/node": "^26.1.2",
231
235
  oxfmt: "^0.61.0",
@@ -420,9 +424,9 @@ function member(name, category, summary, environment = "core") {
420
424
  * @remarks
421
425
  * `version` / `engines` default `DEFAULT_VERSION` / `DEFAULT_ENGINES`,
422
426
  * `src` defaults `['core']`, and `app` / `keywords` / `dependencies` /
423
- * `peers` / `extras` / `overrides` default `[]`. `description` is OMITTED
424
- * entirely when absent, so the result round-trips the exact-record
425
- * `Blueprint` guard.
427
+ * `peers` / `extras` / `overrides` default `[]`, and `bin` / `integration` /
428
+ * `service` default `false`. `description` is OMITTED entirely when absent, so
429
+ * the result round-trips the exact-record `Blueprint` guard.
426
430
  * @returns A complete `Blueprint`.
427
431
  *
428
432
  * @example
@@ -444,7 +448,9 @@ function blueprint(name, options) {
444
448
  version: options?.version ?? "0.0.1",
445
449
  engines: options?.engines ?? DEFAULT_ENGINES,
446
450
  overrides: options?.overrides ?? [],
447
- engine: options?.engine ?? false
451
+ bin: options?.bin ?? false,
452
+ integration: options?.integration ?? false,
453
+ service: options?.service ?? false
448
454
  };
449
455
  return options?.description === void 0 ? base : {
450
456
  ...base,
@@ -1795,7 +1801,9 @@ function blueprintShape() {
1795
1801
  max: MAX_RANGE_LENGTH
1796
1802
  }),
1797
1803
  overrides: (0, _orkestrel_contract.arrayShape)(overrideShape(), { max: MAX_COLLECTION_ITEMS }),
1798
- engine: (0, _orkestrel_contract.booleanShape)()
1804
+ bin: (0, _orkestrel_contract.booleanShape)(),
1805
+ integration: (0, _orkestrel_contract.booleanShape)(),
1806
+ service: (0, _orkestrel_contract.booleanShape)()
1799
1807
  });
1800
1808
  }
1801
1809
  /**
@@ -5523,24 +5531,28 @@ function compareCodeUnit(a, b) {
5523
5531
  return a < b ? -1 : a > b ? 1 : 0;
5524
5532
  }
5525
5533
  /**
5526
- * The host-neutral devDependency baseline every generated workspace needs.
5527
- * Browser providers are added only by browser selections or the scaffold
5528
- * engine's generated-browser consumer proof. A package's `extras` (code-unit
5529
- * sorted) merge in on top, the extras' declared range winning on a name
5530
- * collision with the baseline.
5534
+ * The complete development dependency set one blueprint emits. The shared
5535
+ * baseline is extended by package extras, dev-installed peers, selected
5536
+ * browser environments, and the bin axis's browser test provider.
5531
5537
  *
5532
- * @param extras - The blueprint's package-specific `extras` `Dependency[]`.
5538
+ * @param spec - The blueprint whose development dependencies are required.
5533
5539
  * @returns The merged `devDependencies` record.
5534
5540
  *
5535
5541
  * @example
5536
5542
  * ```ts
5537
- * devDependenciesFor([])['typescript'] // '^6.0.3'
5543
+ * devDependenciesFor(blueprint('router'))['typescript'] // '^6.0.3'
5538
5544
  * ```
5539
5545
  */
5540
- function devDependenciesFor(extras) {
5541
- const baseline = { ...BASE_DEV_DEPENDENCIES };
5542
- for (const extra of [...extras].sort((a, b) => compareCodeUnit(a.name, b.name))) baseline[extra.name] = extra.range;
5543
- return baseline;
5546
+ function devDependenciesFor(spec) {
5547
+ const dependencies = { ...BASE_DEV_DEPENDENCIES };
5548
+ for (const extra of [...spec.extras].sort((a, b) => compareCodeUnit(a.name, b.name))) dependencies[extra.name] = extra.range;
5549
+ for (const peer of [...spec.peers].sort((a, b) => compareCodeUnit(a.name, b.name))) dependencies[peer.name] = peer.range;
5550
+ return {
5551
+ ...dependencies,
5552
+ ...spec.src.includes("browser") ? SOURCE_BROWSER_DEV_DEPENDENCIES : {},
5553
+ ...spec.app.includes("browser") ? APP_BROWSER_DEV_DEPENDENCIES : {},
5554
+ ...spec.bin ? { "@vitest/browser-playwright": SOURCE_BROWSER_DEV_DEPENDENCIES["@vitest/browser-playwright"] } : {}
5555
+ };
5544
5556
  }
5545
5557
  /**
5546
5558
  * Compute the `package.json` artifact's `content`, applying the manifest and
@@ -5565,24 +5577,22 @@ function packageManifest(spec) {
5565
5577
  for (const peer of [...spec.peers].sort((a, b) => compareCodeUnit(a.name, b.name))) peerDependencies[peer.name] = peer.range;
5566
5578
  const peerDependenciesMeta = {};
5567
5579
  for (const peer of spec.peers) if (peer.optional === true) peerDependenciesMeta[peer.name] = { optional: true };
5568
- const peerDevDependencies = {};
5569
- for (const peer of [...spec.peers].sort((a, b) => compareCodeUnit(a.name, b.name))) peerDevDependencies[peer.name] = peer.range;
5570
5580
  const scripts = {
5571
5581
  clean: "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
5572
5582
  copy: "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
5573
- scaffold: spec.engine ? "node ./dist/bin/scaffold.js" : "scaffold",
5583
+ scaffold: spec.bin ? "node ./dist/bin/scaffold.js" : "scaffold",
5574
5584
  lint: "oxlint --config .oxlintrc.json --fix --deny-warnings .",
5575
5585
  check: [
5576
5586
  "tsc --noEmit --project tsconfig.json",
5577
- ...hasSource || spec.engine ? ["npm run check:src"] : [],
5587
+ ...hasSource || spec.bin ? ["npm run check:src"] : [],
5578
5588
  ...spec.app.length > 0 ? ["npm run check:app"] : []
5579
5589
  ].join(" && ")
5580
5590
  };
5581
- if (hasSource || spec.engine) {
5582
- scripts["check:src"] = spec.src.map((environment) => `npm run check:src:${environment}`).join(" && ") + (spec.engine ? `${spec.src.length > 0 ? " && " : ""}npm run check:src:bin` : "");
5591
+ if (hasSource || spec.bin) {
5592
+ scripts["check:src"] = spec.src.map((environment) => `npm run check:src:${environment}`).join(" && ") + (spec.bin ? `${spec.src.length > 0 ? " && " : ""}npm run check:src:bin` : "");
5583
5593
  for (const environment of spec.src) scripts[`check:src:${environment}`] = `tsc --noEmit -p configs/src/tsconfig.${environment}.json`;
5584
5594
  }
5585
- if (spec.engine) scripts["check:src:bin"] = "tsc --noEmit -p configs/src/tsconfig.bin.json";
5595
+ if (spec.bin) scripts["check:src:bin"] = "tsc --noEmit -p configs/src/tsconfig.bin.json";
5586
5596
  if (spec.app.length > 0) {
5587
5597
  scripts["check:app"] = spec.app.map((environment) => `npm run check:app:${environment}`).join(" && ");
5588
5598
  for (const environment of spec.app) scripts[`check:app:${environment}`] = environment === "browser" ? "vue-tsc --noEmit -p configs/app/tsconfig.browser.json" : `tsc --noEmit -p configs/app/tsconfig.${environment}.json`;
@@ -5591,18 +5601,19 @@ function packageManifest(spec) {
5591
5601
  scripts["format:check"] = "oxfmt --config .oxfmtrc.json --check .";
5592
5602
  scripts["lint:check"] = "oxlint --config .oxlintrc.json --deny-warnings .";
5593
5603
  scripts.test = [
5594
- ...hasSource || spec.engine ? ["npm run test:src"] : [],
5604
+ ...hasSource || spec.bin ? ["npm run test:src"] : [],
5595
5605
  ...spec.app.length > 0 ? ["npm run test:app"] : [],
5596
5606
  "npm run test:policy",
5597
5607
  "npm run test:guides"
5598
5608
  ].join(" && ");
5599
- if (hasSource || spec.engine) {
5600
- scripts["test:src"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.src.map((environment) => `--project src:${environment}`).join(" ") + (spec.engine ? `${spec.src.length > 0 ? " " : ""}--project src:bin` : "");
5609
+ if (hasSource || spec.bin) {
5610
+ scripts["test:src"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.src.map((environment) => `--project src:${environment}`).join(" ") + (spec.bin ? `${spec.src.length > 0 ? " " : ""}--project src:bin` : "");
5601
5611
  for (const environment of spec.src) scripts[`test:src:${environment}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project src:${environment}`;
5602
5612
  }
5603
- if (spec.engine) scripts["test:src:bin"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:bin";
5604
- if (spec.engine) scripts["test:integration"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project integration";
5605
- if (spec.engine) scripts["test:equivalence"] = "node -e \"const c=require('node:child_process'),n=process.platform==='win32'?'npm.cmd':'npm',r=c.spawnSync(n,['run','test:integration'],{stdio:'inherit',env:{...process.env,SCAFFOLD_BOUNDARY_EQUIVALENCE:'1'}});process.exit(r.status??1)\"";
5613
+ if (spec.bin) scripts["test:src:bin"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:bin";
5614
+ if (spec.integration) scripts["test:integration"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project integration";
5615
+ if (spec.bin && spec.integration) scripts["test:equivalence"] = "node -e \"const c=require('node:child_process'),n=process.platform==='win32'?'npm.cmd':'npm',r=c.spawnSync(n,['run','test:integration'],{stdio:'inherit',env:{...process.env,SCAFFOLD_BOUNDARY_EQUIVALENCE:'1'}});process.exit(r.status??1)\"";
5616
+ if (spec.service) scripts["test:service"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project service";
5606
5617
  if (spec.app.length > 0) {
5607
5618
  scripts["test:app"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.app.map((environment) => `--project ${APP_MATRIX[environment].project}`).join(" ");
5608
5619
  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}`;
@@ -5611,12 +5622,12 @@ function packageManifest(spec) {
5611
5622
  scripts["test:guides"] = "vitest run --config vite.config.ts --reporter=dot --project guides";
5612
5623
  scripts.build = [
5613
5624
  "npm run clean",
5614
- ...hasSource || spec.engine ? ["npm run build:src"] : [],
5625
+ ...hasSource || spec.bin ? ["npm run build:src"] : [],
5615
5626
  ...spec.app.length > 0 ? ["npm run build:app"] : [],
5616
- ...spec.engine ? ["npm run build:host"] : []
5627
+ ...spec.bin ? ["npm run build:host"] : []
5617
5628
  ].join(" && ");
5618
- if (hasSource || spec.engine) {
5619
- scripts["build:src"] = spec.src.map((environment) => `npm run build:src:${environment}`).join(" && ") + (spec.engine ? `${spec.src.length > 0 ? " && " : ""}npm run build:src:bin` : "");
5629
+ if (hasSource || spec.bin) {
5630
+ scripts["build:src"] = spec.src.map((environment) => `npm run build:src:${environment}`).join(" && ") + (spec.bin ? `${spec.src.length > 0 ? " && " : ""}npm run build:src:bin` : "");
5620
5631
  for (const environment of spec.src) scripts[`build:src:${environment}`] = environment === "browser" ? `vite build --config configs/src/vite.${environment}.config.ts` : `vite build --config configs/src/vite.${environment}.config.ts && npm run copy dist/src/${environment}/index.d.ts dist/src/${environment}/index.d.cts`;
5621
5632
  }
5622
5633
  if (spec.app.length > 0) {
@@ -5629,18 +5640,12 @@ function packageManifest(spec) {
5629
5640
  scripts["serve:build"] = "npm run build:app:server && npm run serve";
5630
5641
  }
5631
5642
  }
5632
- if (spec.engine) {
5643
+ if (spec.bin) {
5633
5644
  scripts["build:src:bin"] = "vite build --config configs/src/vite.bin.config.ts";
5634
5645
  scripts["build:host"] = "node -e \"import('./dist/src/server/index.js').then((m)=>{const n=m.stageHost(process.cwd(),'dist/host').length;console.log('build-host: staged '+n+' file(s) into dist/host')})\"";
5635
5646
  }
5636
- scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.engine ? " && npm run test:integration" : "");
5637
- const devDependencies = {
5638
- ...devDependenciesFor(spec.extras),
5639
- ...peerDevDependencies,
5640
- ...spec.src.includes("browser") ? SOURCE_BROWSER_DEV_DEPENDENCIES : {},
5641
- ...spec.app.includes("browser") ? APP_BROWSER_DEV_DEPENDENCIES : {},
5642
- ...spec.engine ? SOURCE_BROWSER_DEV_DEPENDENCIES : {}
5643
- };
5647
+ scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "");
5648
+ const devDependencies = devDependenciesFor(spec);
5644
5649
  const manifest = {
5645
5650
  name: hasSource ? `@orkestrel/${spec.name}` : spec.name,
5646
5651
  version: spec.version,
@@ -5654,15 +5659,15 @@ function packageManifest(spec) {
5654
5659
  type: "git",
5655
5660
  url: `git+https://github.com/orkestrel/${spec.name}.git`
5656
5661
  },
5657
- ...spec.engine ? { bin: { scaffold: "./dist/bin/scaffold.js" } } : {},
5658
- files: spec.engine ? [
5662
+ ...spec.bin ? { bin: { scaffold: "./dist/bin/scaffold.js" } } : {},
5663
+ files: spec.bin ? [
5659
5664
  "dist/src",
5660
5665
  "dist/bin",
5661
5666
  "dist/host",
5662
5667
  "README.md"
5663
5668
  ] : hasSource ? ["dist/src", "README.md"] : ["dist/app", "README.md"],
5664
5669
  type: "module",
5665
- ...hasSource ? { sideEffects: spec.engine ? ["./src/bin/scaffold.ts", "./dist/bin/scaffold.js"] : false } : {},
5670
+ ...hasSource ? { sideEffects: spec.bin ? ["./src/bin/scaffold.ts", "./dist/bin/scaffold.js"] : false } : {},
5666
5671
  ...entry === void 0 ? {} : {
5667
5672
  main: entry.main,
5668
5673
  module: entry.module,
@@ -5672,7 +5677,7 @@ function packageManifest(spec) {
5672
5677
  },
5673
5678
  scripts,
5674
5679
  dependencies,
5675
- devDependencies: Object.fromEntries(Object.entries(devDependencies).filter(([depName]) => !spec.engine || depName !== "@orkestrel/scaffold").filter(([depName]) => hasSource || depName !== "@microsoft/api-extractor" && depName !== "vite-plugin-dts").filter(([depName]) => hasSource || spec.app.includes("browser") || depName !== "@vitest/browser-playwright").sort(([a], [b]) => compareCodeUnit(a, b))),
5680
+ devDependencies: Object.fromEntries(Object.entries(devDependencies).filter(([depName]) => !spec.bin || depName !== "@orkestrel/scaffold").filter(([depName]) => hasSource || depName !== "@microsoft/api-extractor" && depName !== "vite-plugin-dts").filter(([depName]) => hasSource || spec.app.includes("browser") || depName !== "@vitest/browser-playwright").sort(([a], [b]) => compareCodeUnit(a, b))),
5676
5681
  ...Object.keys(peerDependencies).length > 0 ? { peerDependencies } : {},
5677
5682
  ...Object.keys(peerDependenciesMeta).length > 0 ? { peerDependenciesMeta } : {},
5678
5683
  engines: { node: spec.engines }
@@ -5745,7 +5750,7 @@ function rootTsconfig(src, app = []) {
5745
5750
  *
5746
5751
  * @param src - The declared published `Environment[]`.
5747
5752
  * @param app - The declared application `Environment[]`, defaulting to none.
5748
- * @param engine - Whether the workspace also builds its own executable.
5753
+ * @param bin - Whether the workspace also builds its own executable.
5749
5754
  * @returns The machinery set the generated header renders.
5750
5755
  *
5751
5756
  * @example
@@ -5754,8 +5759,8 @@ function rootTsconfig(src, app = []) {
5754
5759
  * viteMachinery([], ['core']) // { browser: false, vue: false, output: false }
5755
5760
  * ```
5756
5761
  */
5757
- function viteMachinery(src, app = [], engine = false) {
5758
- const unbuilt = src.length === 0 && app.length > 0 && !engine && app.every((environment) => environment === "core");
5762
+ function viteMachinery(src, app = [], bin = false) {
5763
+ const unbuilt = src.length === 0 && app.length > 0 && !bin && app.every((environment) => environment === "core");
5759
5764
  return {
5760
5765
  browser: src.includes("browser") || app.includes("browser"),
5761
5766
  vue: app.includes("browser"),
@@ -5763,6 +5768,65 @@ function viteMachinery(src, app = [], engine = false) {
5763
5768
  };
5764
5769
  }
5765
5770
  /**
5771
+ * Derive the one ordered Vitest project registration list shared by every
5772
+ * generated root configuration shape.
5773
+ *
5774
+ * @param src - The declared published environments.
5775
+ * @param app - The declared application environments.
5776
+ * @param axes - Optional executable, integration, and service project axes.
5777
+ * @returns Source projects, application projects, proof projects, then optional axis projects.
5778
+ *
5779
+ * @example
5780
+ * ```ts
5781
+ * viteProjectRegistrations(['core'], [], { integration: true })
5782
+ * // [{ project: 'srcCore' }, { project: 'policy' }, { project: 'guides' }, { project: 'integration' }]
5783
+ * ```
5784
+ */
5785
+ function viteProjectRegistrations(src, app = [], axes = {}) {
5786
+ const registrations = [];
5787
+ for (const environment of ENVIRONMENTS) {
5788
+ if (!src.includes(environment)) continue;
5789
+ if (environment === "core") registrations.push({ project: "srcCore" });
5790
+ if (environment === "browser") registrations.push({
5791
+ project: "srcBrowser",
5792
+ browser: SRC_MATRIX.browser.project
5793
+ });
5794
+ if (environment === "server") registrations.push({ project: "srcServer" });
5795
+ }
5796
+ for (const environment of ENVIRONMENTS) {
5797
+ if (!app.includes(environment)) continue;
5798
+ if (environment === "core") registrations.push({ project: "appCore" });
5799
+ if (environment === "browser") registrations.push({
5800
+ project: "appBrowser",
5801
+ browser: APP_MATRIX.browser.project
5802
+ });
5803
+ if (environment === "server") registrations.push({ project: "appServer" });
5804
+ }
5805
+ registrations.push({ project: "policy" }, { project: "guides" });
5806
+ if (axes.bin === true) registrations.push({ project: "srcBin" });
5807
+ if (axes.integration === true) registrations.push({ project: "integration" });
5808
+ if (axes.service === true) registrations.push({ project: "service" });
5809
+ return registrations;
5810
+ }
5811
+ /**
5812
+ * Render the one ordered proof and structural-axis project definition block.
5813
+ *
5814
+ * @param axes - Optional executable, integration, and service project axes.
5815
+ * @returns Policy, guides, then selected axis project definitions, separated by one blank line.
5816
+ *
5817
+ * @example
5818
+ * ```ts
5819
+ * viteProjectDefinitions({ bin: true }).includes('export const srcBin =') // true
5820
+ * ```
5821
+ */
5822
+ function viteProjectDefinitions(axes = {}) {
5823
+ const definitions = [policyViteProject(), guidesViteProject()];
5824
+ if (axes.bin === true) definitions.push(binViteProject());
5825
+ if (axes.integration === true) definitions.push(integrationViteProject(axes));
5826
+ if (axes.service === true) definitions.push(serviceViteProject());
5827
+ return definitions.join("\n");
5828
+ }
5829
+ /**
5766
5830
  * Render the root Vitest project registration, preserving browser ownership
5767
5831
  * supplied by the caller.
5768
5832
  *
@@ -6138,15 +6202,21 @@ import { parse as parseVue } from 'vue/compiler-sfc'
6138
6202
  const environmentBoundary = `
6139
6203
  ${CONST_KEYWORD} WORKSPACE_ROOT = realpathSync.native(dirname(fileURLToPath(import.meta.url)))${needsVue ? `\n${EXPORT_KEYWORD} ${CONST_KEYWORD} IMPORT_META_ENV_PREFIX = 'import.meta.env.'` : ""}
6140
6204
 
6205
+ ${EXPORT_KEYWORD} function fileSystemPath(pathname: string): string {
6206
+ if (!pathname.startsWith('/@fs/')) return pathname
6207
+ const candidate = pathname.slice('/@fs/'.length)
6208
+ // Vite URL normalization can collapse the leading slash of a POSIX absolute path.
6209
+ return candidate.startsWith('/') || /^[A-Za-z]:[\\\\/]/.test(candidate)
6210
+ ? candidate
6211
+ : \`/\${candidate}\`
6212
+ }
6213
+
6141
6214
  ${EXPORT_KEYWORD} function physicalPath(path: string): string {
6142
6215
  const [pathWithoutQuery] = path.split('?')
6143
- const candidate = pathWithoutQuery?.startsWith('/@fs/')
6144
- ? pathWithoutQuery.slice('/@fs/'.length)
6145
- : pathWithoutQuery
6146
- const physicalCandidate =
6147
- candidate !== undefined && /^file:/i.test(candidate) ? fileURLToPath(candidate) : candidate
6216
+ const candidate = fileSystemPath(pathWithoutQuery ?? path)
6217
+ const physicalCandidate = /^file:/i.test(candidate) ? fileURLToPath(candidate) : candidate
6148
6218
  const absoluteCandidate =
6149
- physicalCandidate === undefined || physicalCandidate.length === 0
6219
+ physicalCandidate.length === 0
6150
6220
  ? WORKSPACE_ROOT
6151
6221
  : isAbsolute(physicalCandidate)
6152
6222
  ? physicalCandidate
@@ -6203,7 +6273,7 @@ ${EXPORT_KEYWORD} function isWorkspaceBoundaryModule(id: string): boolean {
6203
6273
  const normalizedId = id.replaceAll('\\\\', '/')
6204
6274
  const [path] = normalizedId.split(/[?#]/)
6205
6275
  if (path === undefined) return false
6206
- let candidate = path.startsWith('/@fs/') ? path.slice('/@fs/'.length) : path
6276
+ let candidate = fileSystemPath(path)
6207
6277
  try {
6208
6278
  if (/^file:/i.test(candidate)) candidate = fileURLToPath(candidate)
6209
6279
  } catch {
@@ -6227,10 +6297,7 @@ ${EXPORT_KEYWORD} function isWorkspaceBoundaryModule(id: string): boolean {
6227
6297
  ${EXPORT_KEYWORD} function isOutsideWorkspacePath(path: string): boolean {
6228
6298
  const [pathWithoutQuery] = path.split('?')
6229
6299
  if (pathWithoutQuery === undefined) return false
6230
- const candidate = pathWithoutQuery.startsWith('/@fs/')
6231
- ? pathWithoutQuery.slice('/@fs/'.length)
6232
- : pathWithoutQuery
6233
- return isAbsolute(candidate)
6300
+ return isAbsolute(fileSystemPath(pathWithoutQuery))
6234
6301
  }
6235
6302
 
6236
6303
  ${EXPORT_KEYWORD} function containedPath(root: string, target: string): boolean {
@@ -6280,7 +6347,7 @@ ${EXPORT_KEYWORD} function browserServerPath(
6280
6347
  try {
6281
6348
  const decoded = decodeURIComponent(pathname)
6282
6349
  if (decoded.startsWith('/@fs/')) {
6283
- const candidate = decoded.slice('/@fs/'.length)
6350
+ const candidate = fileSystemPath(decoded)
6284
6351
  if (candidate.length === 0) return null
6285
6352
  return physicalPath(candidate)
6286
6353
  }
@@ -6299,8 +6366,13 @@ ${EXPORT_KEYWORD} function browserServerPath(
6299
6366
  return undefined
6300
6367
  }
6301
6368
  if (/^\\/@(?:app|src)\\//.test(decoded)) return null
6369
+ // Vite owns the app root request, while Vitest owns its in-memory tester root request.
6370
+ if (decoded === '/') return undefined
6302
6371
  if (!decoded.startsWith('/')) return null
6303
- return physicalPath(resolvePath(root, decoded.slice(1)))
6372
+ const candidate = physicalPath(resolvePath(root, decoded.slice(1)))
6373
+ if (existsSync(candidate) || !existsSync(decoded)) return candidate
6374
+ // An absolute module URL denotes itself; browserServerRoots still bounds it.
6375
+ return physicalPath(decoded)
6304
6376
  } catch {
6305
6377
  return null
6306
6378
  }
@@ -6801,6 +6873,12 @@ ${EXPORT_KEYWORD} function maskIgnoredHtml(environmentKeys: ReadonlySet<string>,
6801
6873
  )
6802
6874
  }
6803
6875
 
6876
+ ${EXPORT_KEYWORD} function isBrowserHtmlEntry(filename: string): boolean {
6877
+ return (
6878
+ physicalPath(filename) === physicalPath(resolvePath(WORKSPACE_ROOT, 'app/browser/index.html'))
6879
+ )
6880
+ }
6881
+
6804
6882
  ${EXPORT_KEYWORD} function prepareHtml(): Plugin {
6805
6883
  const environmentKeys = new Set<string>()
6806
6884
  return {
@@ -6816,7 +6894,10 @@ ${EXPORT_KEYWORD} function prepareHtml(): Plugin {
6816
6894
  },
6817
6895
  transformIndexHtml: {
6818
6896
  order: 'pre',
6819
- handler: maskIgnoredHtml.bind(undefined, environmentKeys),
6897
+ handler(html, context) {
6898
+ if (!isBrowserHtmlEntry(context.filename)) return undefined
6899
+ return maskIgnoredHtml(environmentKeys, html)
6900
+ },
6820
6901
  },
6821
6902
  }
6822
6903
  }
@@ -6825,7 +6906,10 @@ ${EXPORT_KEYWORD} function restoreHtml(): Plugin {
6825
6906
  return {
6826
6907
  name: 'orkestrel-html-boundary-restore',
6827
6908
  enforce: 'pre',
6828
- transformIndexHtml: restoreIgnoredHtml,
6909
+ transformIndexHtml(html, context) {
6910
+ if (!isBrowserHtmlEntry(context.filename)) return undefined
6911
+ return restoreIgnoredHtml(html)
6912
+ },
6829
6913
  }
6830
6914
  }
6831
6915
 
@@ -6835,7 +6919,8 @@ ${EXPORT_KEYWORD} function finalizeHtml(): Plugin {
6835
6919
  enforce: 'post',
6836
6920
  transformIndexHtml: {
6837
6921
  order: 'post',
6838
- handler(html) {
6922
+ handler(html, context) {
6923
+ if (!isBrowserHtmlEntry(context.filename)) return undefined
6839
6924
  if (!html.includes(HTML_SECURITY_META)) {
6840
6925
  throw new Error(
6841
6926
  '[orkestrel-environment-boundary] Browser HTML must retain its security policy',
@@ -7252,11 +7337,28 @@ ${EXPORT_KEYWORD} ${CONST_KEYWORD} ENVIRONMENT_CSS = Object.freeze({
7252
7337
  },
7253
7338
  },
7254
7339
  } satisfies CSSOptions)
7340
+
7341
+ /** Prevent the Vitest browser mid-run "optimized dependencies changed, reloading" stall. */
7342
+ ${EXPORT_KEYWORD} ${CONST_KEYWORD} BROWSER_TEST_DEPENDENCIES = Object.freeze([
7343
+ '@vitest/browser/client',
7344
+ 'vitest/browser',
7345
+ 'vitest/internal/browser',
7346
+ 'vitest',
7347
+ ])
7255
7348
  ` : ""}${EXPORT_KEYWORD} ${CONST_KEYWORD} PACKAGE_MANIFEST_BYTES = 1_048_576
7256
7349
  ${EXPORT_KEYWORD} ${CONST_KEYWORD} ENVIRONMENT_MODULE_BYTES = 8_388_608
7257
7350
  ${environmentBoundary}`;
7258
7351
  }
7259
- /** Build the dedicated Node-only repository policy Vitest project. */
7352
+ /**
7353
+ * Build the dedicated standalone Node-only repository-policy Vitest project.
7354
+ *
7355
+ * @returns The emitted `policy` project definition.
7356
+ *
7357
+ * @example
7358
+ * ```ts
7359
+ * policyViteProject().includes("label: 'policy'") // true
7360
+ * ```
7361
+ */
7260
7362
  function policyViteProject() {
7261
7363
  return `${EXPORT_KEYWORD} const policy = (config?: UserConfig): UserConfig =>
7262
7364
  mergeConfig(
@@ -7275,12 +7377,150 @@ function policyViteProject() {
7275
7377
  `;
7276
7378
  }
7277
7379
  /**
7380
+ * Build the standalone Node-only guide-parity Vitest project.
7381
+ *
7382
+ * @returns The emitted `guides` project definition.
7383
+ *
7384
+ * @example
7385
+ * ```ts
7386
+ * guidesViteProject().includes("label: 'guides'") // true
7387
+ * ```
7388
+ */
7389
+ function guidesViteProject() {
7390
+ return `${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
7391
+ mergeConfig(
7392
+ {
7393
+ resolve,
7394
+ test: {
7395
+ name: { label: 'guides', color: 'green' },
7396
+ include: ['tests/guides/**/*.test.ts'],
7397
+ exclude: ['tests/src/**/*.test.ts', 'tests/app/**/*.test.ts', 'tests/setup.test.ts'],
7398
+ setupFiles: ['./tests/setup.ts'],
7399
+ environment: 'node',
7400
+ browser: { enabled: false },
7401
+ },
7402
+ },
7403
+ config ?? {},
7404
+ )
7405
+ `;
7406
+ }
7407
+ /**
7408
+ * Build the executable's dedicated Node-only build and test project.
7409
+ *
7410
+ * @returns The emitted `srcBin` project definition.
7411
+ *
7412
+ * @example
7413
+ * ```ts
7414
+ * binViteProject().includes("label: 'src:bin'") // true
7415
+ * ```
7416
+ */
7417
+ function binViteProject() {
7418
+ return `${EXPORT_KEYWORD} const srcBin = (config?: UserConfig): UserConfig =>
7419
+ mergeConfig(
7420
+ {
7421
+ resolve,
7422
+ publicDir: false,
7423
+ plugins: [outputBoundary('dist/bin')],
7424
+ build: {
7425
+ emptyOutDir: true,
7426
+ sourcemap: true,
7427
+ minify: false,
7428
+ lib: {
7429
+ entry: resolveWorkspacePath('src/bin/scaffold.ts'),
7430
+ formats: ['es'],
7431
+ fileName: () => 'scaffold.js',
7432
+ },
7433
+ outDir: 'dist/bin',
7434
+ target: 'node22',
7435
+ rolldownOptions: { external: [/^node:/, /^@orkestrel\\//, /^@src\\//] },
7436
+ },
7437
+ test: {
7438
+ name: { label: 'src:bin', color: 'yellow' },
7439
+ include: ['tests/src/bin/**/*.test.ts'],
7440
+ setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
7441
+ environment: 'node',
7442
+ browser: { enabled: false },
7443
+ },
7444
+ },
7445
+ config ?? {},
7446
+ )
7447
+ `;
7448
+ }
7449
+ /**
7450
+ * Build the standalone Node-only installed-consumer integration proof project.
7451
+ *
7452
+ * @param axes - Optional executable and integration axes controlling the shared registry setup.
7453
+ * @returns The emitted `integration` project definition.
7454
+ *
7455
+ * @example
7456
+ * ```ts
7457
+ * integrationViteProject({ bin: true, integration: true }).includes(
7458
+ * "globalSetup: ['./tests/setupIntegration.ts']",
7459
+ * ) // true
7460
+ * ```
7461
+ */
7462
+ function integrationViteProject(axes = {}) {
7463
+ return `${EXPORT_KEYWORD} const integration = (config?: UserConfig): UserConfig =>
7464
+ mergeConfig(
7465
+ {
7466
+ resolve,
7467
+ test: {
7468
+ name: { label: 'integration', color: 'blue' },
7469
+ include: ['tests/integration/**/*.test.ts'],
7470
+ setupFiles: ['./tests/setup.ts'],
7471
+ ${axes.bin === true && axes.integration === true ? ` // Wire the template registry for the generated-consumer proof.
7472
+ globalSetup: ['./tests/setupIntegration.ts'],
7473
+ ` : ""} environment: 'node',
7474
+ browser: { enabled: false },
7475
+ testTimeout: 120_000,
7476
+ hookTimeout: 120_000,
7477
+ fileParallelism: false,
7478
+ },
7479
+ },
7480
+ config ?? {},
7481
+ )
7482
+ `;
7483
+ }
7484
+ /**
7485
+ * Build the standalone Node-only live-service proof project.
7486
+ *
7487
+ * @returns The emitted `service` project definition.
7488
+ *
7489
+ * @example
7490
+ * ```ts
7491
+ * serviceViteProject().includes("label: 'service'") // true
7492
+ * ```
7493
+ */
7494
+ function serviceViteProject() {
7495
+ return `${EXPORT_KEYWORD} const service = (config?: UserConfig): UserConfig =>
7496
+ mergeConfig(
7497
+ {
7498
+ resolve,
7499
+ test: {
7500
+ name: { label: 'service', color: 'red' },
7501
+ include: ['tests/service/**/*.test.ts'],
7502
+ setupFiles: ['./tests/setup.ts', './tests/setupService.ts'],
7503
+ environment: 'node',
7504
+ browser: { enabled: false },
7505
+ testTimeout: 120_000,
7506
+ hookTimeout: 120_000,
7507
+ fileParallelism: false,
7508
+ },
7509
+ },
7510
+ config ?? {},
7511
+ )
7512
+ `;
7513
+ }
7514
+ /**
7278
7515
  * The single non-`core` environment's factory IS the base (Shape 3 of
7279
7516
  * `rootViteConfig`) — the environment's own `viteHeader` (Playwright only when
7280
7517
  * `environment === 'browser'`, per the live sqlite/indexeddb exemplars) prefixes
7281
- * the environment-specific `srcBrowser` / `srcServer` + `guides` projects export.
7518
+ * the environment-specific `srcBrowser` / `srcServer` project, followed by the
7519
+ * standalone policy and guides proof projects and any selected structural-axis
7520
+ * projects.
7282
7521
  *
7283
7522
  * @param environment - The sole declared non-`core` environment.
7523
+ * @param axes - Optional executable, integration, and service project axes.
7284
7524
  * @returns The root `vite.config.ts` file content for a single non-`core` environment, newline-terminated.
7285
7525
  *
7286
7526
  * @example
@@ -7288,9 +7528,11 @@ function policyViteProject() {
7288
7528
  * singleSrcViteConfig('server').includes('srcServer') // true
7289
7529
  * ```
7290
7530
  */
7291
- function singleSrcViteConfig(environment) {
7531
+ function singleSrcViteConfig(environment, axes = {}) {
7292
7532
  const machinery = viteMachinery([environment]);
7293
7533
  const header = viteHeader(machinery);
7534
+ const renderedTest = renderViteTest(viteProjectRegistrations([environment], [], axes), machinery.browser);
7535
+ const definitions = viteProjectDefinitions(axes);
7294
7536
  if (environment === "browser") return `${header}
7295
7537
  ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7296
7538
  mergeConfig(
@@ -7318,6 +7560,18 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7318
7560
  name: { label: 'src:browser', color: 'yellow' },
7319
7561
  include: ['tests/src/browser/**/*.test.ts'],
7320
7562
  setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
7563
+ ...(config?.test?.browser?.enabled === false
7564
+ ? {}
7565
+ : {
7566
+ deps: {
7567
+ optimizer: {
7568
+ client: {
7569
+ enabled: true,
7570
+ include: [...BROWSER_TEST_DEPENDENCIES],
7571
+ },
7572
+ },
7573
+ },
7574
+ }),
7321
7575
  browser: {
7322
7576
  enabled: true,
7323
7577
  provider: playwright(),
@@ -7329,34 +7583,10 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7329
7583
  config ?? {},
7330
7584
  )
7331
7585
 
7332
- ${policyViteProject()}
7333
- ${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
7334
- srcBrowser(
7335
- mergeConfig(
7336
- {
7337
- test: {
7338
- name: { label: 'guides', color: 'green' },
7339
- include: ['tests/guides/**/*.test.ts'],
7340
- exclude: ['tests/src/**/*.test.ts', 'tests/setup.test.ts'],
7341
- environment: 'node',
7342
- browser: { enabled: false },
7343
- },
7344
- },
7345
- config ?? {},
7346
- ),
7347
- )
7348
-
7586
+ ${definitions}
7349
7587
  export default defineConfig({
7350
7588
  resolve,
7351
- test: gateBrowserProjects(
7352
- [
7353
- { project: srcBrowser, browser: 'src:browser' },
7354
- { project: policy },
7355
- { project: guides },
7356
- ],
7357
- hasChromium,
7358
- process.argv,
7359
- ),
7589
+ ${renderedTest}
7360
7590
  })
7361
7591
  `;
7362
7592
  return `${header}
@@ -7392,33 +7622,18 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
7392
7622
  config ?? {},
7393
7623
  )
7394
7624
 
7395
- ${policyViteProject()}
7396
- ${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
7397
- srcServer(
7398
- mergeConfig(
7399
- {
7400
- test: {
7401
- name: { label: 'guides', color: 'green' },
7402
- include: ['tests/guides/**/*.test.ts'],
7403
- exclude: ['tests/src/**/*.test.ts', 'tests/setup.test.ts'],
7404
- },
7405
- },
7406
- config ?? {},
7407
- ),
7408
- )
7409
-
7625
+ ${definitions}
7410
7626
  export default defineConfig({
7411
7627
  resolve,
7412
- test: {
7413
- projects: [srcServer, policy, guides],
7414
- },
7628
+ ${renderedTest}
7415
7629
  })
7416
7630
  `;
7417
7631
  }
7418
7632
  /**
7419
7633
  * The root `vite.config.ts` — three grounded shapes, chosen by a blueprint's
7420
7634
  * `src`:
7421
- * 1. `core`-only — `srcCore` + `guides`, no Playwright at all (the live
7635
+ * 1. `core`-only — `srcCore` + standalone policy/guides proof projects, no
7636
+ * Playwright at all (the live
7422
7637
  * timeout exemplar: no browser project exists anywhere in the file).
7423
7638
  * 2. Multi-environment (2+ src, always including `core` per the live
7424
7639
  * middleware/router exemplars) — `srcCore` is the shared base;
@@ -7433,10 +7648,9 @@ export default defineConfig({
7433
7648
  * environment is `browser` (it must run its own tests in a real browser).
7434
7649
  *
7435
7650
  * @param src - The declared `Environment[]`.
7436
- * @param engine - Structural: `true` appends the `srcBin` project (an
7437
- * executable build target, never a barrel) after the other declared
7438
- * projects the self-hosting tax, grounded against this very repo's own
7439
- * checked-in `vite.config.ts`.
7651
+ * @param axes - Optional structural project axes. `bin` appends the standalone
7652
+ * executable build-and-test project; `integration` and `service` append their
7653
+ * standalone proof projects.
7440
7654
  * @returns The root `vite.config.ts` file content, newline-terminated.
7441
7655
  *
7442
7656
  * @example
@@ -7444,14 +7658,14 @@ export default defineConfig({
7444
7658
  * rootViteConfig(['core']).includes('srcCore') // true
7445
7659
  * ```
7446
7660
  */
7447
- function rootViteConfig(src, engine = false) {
7661
+ function rootViteConfig(src, axes = {}) {
7448
7662
  const hasCore = src.includes("core");
7449
- const nonCore = src.filter((environment) => environment !== "core");
7663
+ const nonCore = ENVIRONMENTS.filter((environment) => environment !== "core" && src.includes(environment));
7450
7664
  const machinery = viteMachinery(src);
7451
7665
  const header = viteHeader(machinery);
7452
7666
  if (!hasCore) {
7453
7667
  const [onlyEnvironment] = nonCore;
7454
- if (onlyEnvironment === "browser" || onlyEnvironment === "server") return singleSrcViteConfig(onlyEnvironment);
7668
+ if (onlyEnvironment === "browser" || onlyEnvironment === "server") return singleSrcViteConfig(onlyEnvironment, axes);
7455
7669
  }
7456
7670
  const browserBlock = `
7457
7671
  ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
@@ -7479,6 +7693,18 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7479
7693
  include: ['tests/src/browser/**/*.test.ts'],
7480
7694
  exclude: ['tests/src/core/**/*.test.ts'],
7481
7695
  setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
7696
+ ...(config?.test?.browser?.enabled === false
7697
+ ? {}
7698
+ : {
7699
+ deps: {
7700
+ optimizer: {
7701
+ client: {
7702
+ enabled: true,
7703
+ include: [...BROWSER_TEST_DEPENDENCIES],
7704
+ },
7705
+ },
7706
+ },
7707
+ }),
7482
7708
  browser: {
7483
7709
  enabled: true,
7484
7710
  provider: playwright(),
@@ -7533,60 +7759,8 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
7533
7759
  ),
7534
7760
  )
7535
7761
  `;
7536
- const binBlock = engine ? `
7537
- ${EXPORT_KEYWORD} const srcBin = (config?: UserConfig): UserConfig =>
7538
- srcCore(
7539
- mergeConfig(
7540
- {
7541
- publicDir: false,
7542
- plugins: [outputBoundary('dist/bin')],
7543
- build: {
7544
- lib: {
7545
- entry: resolveWorkspacePath('src/bin/scaffold.ts'),
7546
- formats: ['es'],
7547
- fileName: () => 'scaffold.js',
7548
- },
7549
- outDir: 'dist/bin',
7550
- target: 'node22',
7551
- rolldownOptions: {
7552
- external: [/^node:/, /^@orkestrel\\//, /^@src\\//],
7553
- },
7554
- },
7555
- test: {
7556
- name: { label: 'src:bin', color: 'yellow' },
7557
- include: ['tests/src/bin/**/*.test.ts'],
7558
- exclude: ['tests/src/core/**/*.test.ts', 'tests/src/server/**/*.test.ts'],
7559
- setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
7560
- },
7561
- },
7562
- config ?? {},
7563
- ),
7564
- )
7565
-
7566
- ${EXPORT_KEYWORD} const integration = (config?: UserConfig): UserConfig =>
7567
- srcBin(
7568
- mergeConfig(
7569
- {
7570
- test: {
7571
- name: { label: 'integration', color: 'blue' },
7572
- include: ['tests/integration/**/*.test.ts'],
7573
- exclude: ['tests/src/**/*.test.ts', 'tests/guides/**/*.test.ts'],
7574
- },
7575
- },
7576
- config ?? {},
7577
- ),
7578
- )
7579
- ` : "";
7580
- const blocks = nonCore.map((environment) => environment === "browser" ? browserBlock : serverBlock).join("") + binBlock;
7581
- const registrations = [];
7582
- if (hasCore) registrations.push({ project: "srcCore" });
7583
- for (const environment of nonCore) registrations.push(environment === "browser" ? {
7584
- project: "srcBrowser",
7585
- browser: SRC_MATRIX.browser.project
7586
- } : { project: "srcServer" });
7587
- registrations.push({ project: "policy" }, { project: "guides" });
7588
- if (engine) registrations.push({ project: "srcBin" }, { project: "integration" });
7589
- const renderedTest = renderViteTest(registrations, machinery.browser);
7762
+ const blocks = nonCore.map((environment) => environment === "browser" ? browserBlock : serverBlock).join("");
7763
+ const renderedTest = renderViteTest(viteProjectRegistrations(src, [], axes), machinery.browser);
7590
7764
  return `${header}
7591
7765
  ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
7592
7766
  mergeConfig(
@@ -7608,22 +7782,8 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
7608
7782
  },
7609
7783
  config ?? {},
7610
7784
  )
7611
-
7612
- ${policyViteProject()}
7613
- ${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
7614
- srcCore(
7615
- mergeConfig(
7616
- {
7617
- test: {
7618
- name: { label: 'guides', color: 'green' },
7619
- include: ['tests/guides/**/*.test.ts'],
7620
- exclude: ['tests/src/**/*.test.ts', 'tests/setup.test.ts'],
7621
- },
7622
- },
7623
- config ?? {},
7624
- ),
7625
- )
7626
7785
  ${blocks}
7786
+ ${viteProjectDefinitions(axes)}
7627
7787
  export default defineConfig({
7628
7788
  resolve,
7629
7789
  ${renderedTest}
@@ -7636,7 +7796,7 @@ ${renderedTest}
7636
7796
  *
7637
7797
  * @param src - Published src environments.
7638
7798
  * @param app - Private app environments.
7639
- * @param engine - Whether the workspace also builds scaffold's executable.
7799
+ * @param axes - Optional executable, integration, and service project axes.
7640
7800
  * @returns The root `vite.config.ts` content.
7641
7801
  *
7642
7802
  * @example
@@ -7644,15 +7804,12 @@ ${renderedTest}
7644
7804
  * applicationViteConfig([], ['core', 'server']).includes('appServer') // true
7645
7805
  * ```
7646
7806
  */
7647
- function applicationViteConfig(src, app, engine = false) {
7807
+ function applicationViteConfig(src, app, axes = {}) {
7648
7808
  const hasSourceCore = src.includes("core");
7649
- const machinery = viteMachinery(src, app, engine);
7809
+ const machinery = viteMachinery(src, app, axes.bin === true);
7650
7810
  const header = viteHeader(machinery);
7651
- const registrations = [];
7652
7811
  const blocks = [];
7653
- if (src.includes("core")) {
7654
- registrations.push({ project: "srcCore" });
7655
- blocks.push(`
7812
+ if (src.includes("core")) blocks.push(`
7656
7813
  ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
7657
7814
  mergeConfig(
7658
7815
  {
@@ -7671,12 +7828,7 @@ ${EXPORT_KEYWORD} const srcCore = (config?: UserConfig): UserConfig =>
7671
7828
  config ?? {},
7672
7829
  )
7673
7830
  `);
7674
- }
7675
7831
  if (src.includes("browser")) {
7676
- registrations.push({
7677
- project: "srcBrowser",
7678
- browser: SRC_MATRIX.browser.project
7679
- });
7680
7832
  const coreOutput = hasSourceCore ? `
7681
7833
  output: { paths: { '@src/core': '../core/index.js' } },` : "";
7682
7834
  const coreExternal = hasSourceCore ? `id === '@src/core' || ` : "";
@@ -7707,6 +7859,18 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7707
7859
  name: { label: 'src:browser', color: 'yellow' },
7708
7860
  include: ['tests/src/browser/**/*.test.ts'],
7709
7861
  ${hasSourceCore ? "exclude: ['tests/src/core/**/*.test.ts'],\n " : ""}setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
7862
+ ...(config?.test?.browser?.enabled === false
7863
+ ? {}
7864
+ : {
7865
+ deps: {
7866
+ optimizer: {
7867
+ client: {
7868
+ enabled: true,
7869
+ include: [...BROWSER_TEST_DEPENDENCIES],
7870
+ },
7871
+ },
7872
+ },
7873
+ }),
7710
7874
  browser: {
7711
7875
  enabled: true,
7712
7876
  provider: playwright(),
@@ -7720,7 +7884,6 @@ ${EXPORT_KEYWORD} const srcBrowser = (config?: UserConfig): UserConfig =>
7720
7884
  `);
7721
7885
  }
7722
7886
  if (src.includes("server")) {
7723
- registrations.push({ project: "srcServer" });
7724
7887
  const coreOutput = hasSourceCore ? `
7725
7888
  output: [
7726
7889
  {
@@ -7770,9 +7933,7 @@ ${EXPORT_KEYWORD} const srcServer = (config?: UserConfig): UserConfig =>
7770
7933
  )
7771
7934
  `);
7772
7935
  }
7773
- if (app.includes("core")) {
7774
- registrations.push({ project: "appCore" });
7775
- blocks.push(`
7936
+ if (app.includes("core")) blocks.push(`
7776
7937
  ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
7777
7938
  mergeConfig(
7778
7939
  {
@@ -7790,13 +7951,7 @@ ${EXPORT_KEYWORD} const appCore = (config?: UserConfig): UserConfig =>
7790
7951
  config ?? {},
7791
7952
  )
7792
7953
  `);
7793
- }
7794
- if (app.includes("browser")) {
7795
- registrations.push({
7796
- project: "appBrowser",
7797
- browser: APP_MATRIX.browser.project
7798
- });
7799
- blocks.push(`
7954
+ if (app.includes("browser")) blocks.push(`
7800
7955
  ${EXPORT_KEYWORD} function appBrowser(...config: readonly never[]): UserConfig {
7801
7956
  if (config.length > 0) {
7802
7957
  throw new Error(
@@ -7815,7 +7970,6 @@ ${EXPORT_KEYWORD} function appBrowser(...config: readonly never[]): UserConfig {
7815
7970
  prepareHtml(),
7816
7971
  finalizeHtml(),
7817
7972
  ],
7818
- optimizeDeps: { include: ['vue'] },
7819
7973
  root: resolveWorkspacePath('app/browser'),
7820
7974
  publicDir: false,
7821
7975
  server: {
@@ -7838,6 +7992,14 @@ ${EXPORT_KEYWORD} function appBrowser(...config: readonly never[]): UserConfig {
7838
7992
  dir: resolveWorkspacePath('.'),
7839
7993
  include: ['tests/app/browser/**/*.test.ts'],
7840
7994
  setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
7995
+ deps: {
7996
+ optimizer: {
7997
+ client: {
7998
+ enabled: true,
7999
+ include: ['vue', ...BROWSER_TEST_DEPENDENCIES],
8000
+ },
8001
+ },
8002
+ },
7841
8003
  browser: {
7842
8004
  enabled: true,
7843
8005
  provider: playwright(),
@@ -7848,10 +8010,7 @@ ${EXPORT_KEYWORD} function appBrowser(...config: readonly never[]): UserConfig {
7848
8010
  }
7849
8011
  }
7850
8012
  `);
7851
- }
7852
- if (app.includes("server")) {
7853
- registrations.push({ project: "appServer" });
7854
- blocks.push(`
8013
+ if (app.includes("server")) blocks.push(`
7855
8014
  ${EXPORT_KEYWORD} const appServer = (config?: UserConfig): UserConfig =>
7856
8015
  mergeConfig(
7857
8016
  {
@@ -7882,72 +8041,10 @@ ${EXPORT_KEYWORD} const appServer = (config?: UserConfig): UserConfig =>
7882
8041
  config ?? {},
7883
8042
  )
7884
8043
  `);
7885
- }
7886
- if (engine) {
7887
- registrations.push({ project: "srcBin" }, { project: "integration" });
7888
- blocks.push(`
7889
- ${EXPORT_KEYWORD} const srcBin = (config?: UserConfig): UserConfig =>
7890
- mergeConfig(
7891
- {
7892
- resolve,
7893
- publicDir: false,
7894
- plugins: [outputBoundary('dist/bin')],
7895
- build: {
7896
- lib: {
7897
- entry: resolveWorkspacePath('src/bin/scaffold.ts'),
7898
- formats: ['es'],
7899
- fileName: () => 'scaffold.js',
7900
- },
7901
- outDir: 'dist/bin',
7902
- target: 'node22',
7903
- rolldownOptions: { external: [/^node:/, /^@orkestrel\\//, /^@src\\//] },
7904
- },
7905
- test: {
7906
- name: { label: 'src:bin', color: 'yellow' },
7907
- include: ['tests/src/bin/**/*.test.ts'],
7908
- setupFiles: ['./tests/setup.ts', './tests/setupServer.ts'],
7909
- environment: 'node',
7910
- browser: { enabled: false },
7911
- },
7912
- },
7913
- config ?? {},
7914
- )
7915
-
7916
- ${EXPORT_KEYWORD} const integration = (config?: UserConfig): UserConfig =>
7917
- srcBin(
7918
- mergeConfig(
7919
- {
7920
- test: {
7921
- name: { label: 'integration', color: 'blue' },
7922
- include: ['tests/integration/**/*.test.ts'],
7923
- exclude: ['tests/src/**/*.test.ts', 'tests/app/**/*.test.ts', 'tests/guides/**/*.test.ts'],
7924
- },
7925
- },
7926
- config ?? {},
7927
- ),
7928
- )
7929
- `);
7930
- }
7931
- registrations.push({ project: "policy" }, { project: "guides" });
7932
- const renderedTest = renderViteTest(registrations, machinery.browser);
7933
- return `${header}
7934
- ${policyViteProject()}
7935
- ${EXPORT_KEYWORD} const guides = (config?: UserConfig): UserConfig =>
7936
- mergeConfig(
7937
- {
7938
- resolve,
7939
- test: {
7940
- name: { label: 'guides', color: 'green' },
7941
- include: ['tests/guides/**/*.test.ts'],
7942
- exclude: ['tests/src/**/*.test.ts', 'tests/app/**/*.test.ts', 'tests/setup.test.ts'],
7943
- setupFiles: ['./tests/setup.ts'],
7944
- environment: 'node',
7945
- browser: { enabled: false },
7946
- },
7947
- },
7948
- config ?? {},
7949
- )
7950
- ${blocks.join("")}
8044
+ const renderedTest = renderViteTest(viteProjectRegistrations(src, app, axes), machinery.browser);
8045
+ const definitions = viteProjectDefinitions(axes);
8046
+ return `${header}${blocks.join("")}
8047
+ ${definitions}
7951
8048
  export default defineConfig({
7952
8049
  resolve,
7953
8050
  ${renderedTest}
@@ -8101,6 +8198,67 @@ export default defineConfig(
8101
8198
  `;
8102
8199
  }
8103
8200
  /**
8201
+ * Build the executable's declaration-only `configs/src/tsconfig.bin.json`.
8202
+ *
8203
+ * @returns The executable `tsconfig` file content, newline-terminated.
8204
+ *
8205
+ * @example
8206
+ * ```ts
8207
+ * binTsconfig().includes('"outDir": "../../dist/bin"') // true
8208
+ * ```
8209
+ */
8210
+ function binTsconfig() {
8211
+ return formatJson({
8212
+ extends: "../../tsconfig.json",
8213
+ compilerOptions: {
8214
+ lib: ["ESNext"],
8215
+ types: ["node"],
8216
+ noEmit: false,
8217
+ declaration: true,
8218
+ emitDeclarationOnly: true,
8219
+ rootDir: "../../src",
8220
+ outDir: "../../dist/bin"
8221
+ },
8222
+ include: TYPESCRIPT_EXTENSIONS.map((extension) => `../../src/bin/**/*.${extension}`)
8223
+ });
8224
+ }
8225
+ /**
8226
+ * Build the executable's `configs/src/vite.bin.config.ts` wrapper.
8227
+ *
8228
+ * @returns The executable Vite configuration content, newline-terminated.
8229
+ *
8230
+ * @example
8231
+ * ```ts
8232
+ * binViteConfig().includes("banner: '#!/usr/bin/env node'") // true
8233
+ * ```
8234
+ */
8235
+ function binViteConfig() {
8236
+ return `import { defineConfig } from 'vite'
8237
+ import { srcBin } from '../../vite.config'
8238
+
8239
+ // The \`scaffold\` executable build — a single ESM lib file, no declarations (an
8240
+ // executable ships no types), with the \`#!/usr/bin/env node\` shebang re-emitted via
8241
+ // \`output.banner\` (rolldown strips shebangs from source during bundling), and
8242
+ // \`output.paths\` rewriting the externalized \`@src/*\` specifiers to the built sibling
8243
+ // src environments (relative to \`dist/bin/\`), so the emitted bin resolves at runtime.
8244
+ export default defineConfig(
8245
+ srcBin({
8246
+ build: {
8247
+ rolldownOptions: {
8248
+ output: {
8249
+ banner: '#!/usr/bin/env node',
8250
+ paths: {
8251
+ '@src/core': '../src/core/index.js',
8252
+ '@src/server': '../src/server/index.js',
8253
+ },
8254
+ },
8255
+ },
8256
+ },
8257
+ }),
8258
+ )
8259
+ `;
8260
+ }
8261
+ /**
8104
8262
  * Build one `configs/app/tsconfig.<environment>.json` check-only configuration.
8105
8263
  *
8106
8264
  * @param environment - The application environment.
@@ -8149,12 +8307,25 @@ export default defineConfig(${anchor}())
8149
8307
  `;
8150
8308
  }
8151
8309
  /**
8152
- * Build selection-aware GitHub CI without external service dependencies.
8310
+ * Build selection-aware GitHub CI, provisioning the declared foreign service before its proof.
8153
8311
  *
8154
8312
  * @param spec - The workspace blueprint.
8155
8313
  * @returns The complete `.github/workflows/ci.yml` content.
8156
8314
  */
8157
8315
  function ciWorkflow(spec) {
8316
+ const browser = spec.bin || spec.src.includes("browser") || spec.app.includes("browser") ? `
8317
+ - name: Install Playwright browsers
8318
+ run: npx --no-install playwright install --with-deps chromium
8319
+ ` : "";
8320
+ const tail = [];
8321
+ if (spec.integration) tail.push(` - name: Run live consumer integration
8322
+ run: npm run test:integration`);
8323
+ if (spec.service) {
8324
+ tail.push(` - name: Provision live service
8325
+ run: bash ${SERVICE_SCRIPT_PATH}`);
8326
+ tail.push(` - name: Run live service tests
8327
+ run: npm run test:service`);
8328
+ }
8158
8329
  return `name: ci.yml
8159
8330
 
8160
8331
  on:
@@ -8186,10 +8357,7 @@ jobs:
8186
8357
 
8187
8358
  - name: Install dependencies
8188
8359
  run: npm ci --ignore-scripts
8189
- ${spec.engine || spec.src.includes("browser") || spec.app.includes("browser") ? `
8190
- - name: Install Playwright browsers
8191
- run: npx --no-install playwright install --with-deps chromium
8192
- ` : ""}
8360
+ ${browser}
8193
8361
  - name: Check formatting
8194
8362
  run: npm run format:check
8195
8363
 
@@ -8203,11 +8371,7 @@ ${spec.engine || spec.src.includes("browser") || spec.app.includes("browser") ?
8203
8371
  run: npm run build
8204
8372
 
8205
8373
  - name: Run tests
8206
- run: npm test${spec.engine ? `
8207
-
8208
- - name: Run live consumer integration
8209
- run: npm run test:integration
8210
- ` : ""}
8374
+ run: npm test${tail.length === 0 ? "" : `\n\n${tail.join("\n\n")}`}
8211
8375
  `;
8212
8376
  }
8213
8377
  /**
@@ -8225,7 +8389,7 @@ ${spec.engine || spec.src.includes("browser") || spec.app.includes("browser") ?
8225
8389
  * ```
8226
8390
  */
8227
8391
  function configArtifacts(spec) {
8228
- const machinery = viteMachinery(spec.src, spec.app, spec.engine);
8392
+ const machinery = viteMachinery(spec.src, spec.app, spec.bin);
8229
8393
  const artifacts = [{
8230
8394
  path: "tsconfig.json",
8231
8395
  group: "configs",
@@ -8235,7 +8399,7 @@ function configArtifacts(spec) {
8235
8399
  path: "vite.config.ts",
8236
8400
  group: "configs",
8237
8401
  origin: "computed",
8238
- content: spec.app.length > 0 ? applicationViteConfig(spec.src, spec.app, spec.engine) : rootViteConfig(spec.src, spec.engine)
8402
+ content: spec.app.length > 0 ? applicationViteConfig(spec.src, spec.app, spec) : rootViteConfig(spec.src, spec)
8239
8403
  }];
8240
8404
  for (const environment of spec.src) {
8241
8405
  const row = SRC_MATRIX[environment];
@@ -8251,6 +8415,12 @@ function configArtifacts(spec) {
8251
8415
  });
8252
8416
  }
8253
8417
  }
8418
+ if (spec.bin) for (const path of BIN_CONFIGS) artifacts.push({
8419
+ path,
8420
+ group: "configs",
8421
+ origin: "computed",
8422
+ content: path.endsWith(".json") ? binTsconfig() : binViteConfig()
8423
+ });
8254
8424
  for (const environment of spec.app) {
8255
8425
  const row = APP_MATRIX[environment];
8256
8426
  for (const path of row.configs) {
@@ -9399,6 +9569,7 @@ function createBlueprint(data) {
9399
9569
  exports.APP_BROWSER_DEV_DEPENDENCIES = APP_BROWSER_DEV_DEPENDENCIES;
9400
9570
  exports.APP_MATRIX = APP_MATRIX;
9401
9571
  exports.BASE_DEV_DEPENDENCIES = BASE_DEV_DEPENDENCIES;
9572
+ exports.BIN_CONFIGS = BIN_CONFIGS;
9402
9573
  exports.CATEGORIES = CATEGORIES;
9403
9574
  exports.CHECKOUT_ACTION_SHA = CHECKOUT_ACTION_SHA;
9404
9575
  exports.COMPILER_ID = COMPILER_ID;
@@ -9441,6 +9612,7 @@ exports.ORIGINS = ORIGINS;
9441
9612
  exports.ORKESTREL_RANGE_PATTERN = ORKESTREL_RANGE_PATTERN;
9442
9613
  exports.PlanManager = PlanManager;
9443
9614
  exports.SCAFFOLD_RANGE = SCAFFOLD_RANGE;
9615
+ exports.SERVICE_SCRIPT_PATH = SERVICE_SCRIPT_PATH;
9444
9616
  exports.SETUP_NODE_ACTION_SHA = SETUP_NODE_ACTION_SHA;
9445
9617
  exports.SOURCE_BROWSER_DEV_DEPENDENCIES = SOURCE_BROWSER_DEV_DEPENDENCIES;
9446
9618
  exports.SRC_MATRIX = SRC_MATRIX;
@@ -9457,6 +9629,9 @@ exports.applicationViteConfig = applicationViteConfig;
9457
9629
  exports.applyOverrides = applyOverrides;
9458
9630
  exports.artifactShape = artifactShape;
9459
9631
  exports.auditToReview = auditToReview;
9632
+ exports.binTsconfig = binTsconfig;
9633
+ exports.binViteConfig = binViteConfig;
9634
+ exports.binViteProject = binViteProject;
9460
9635
  exports.blueprint = blueprint;
9461
9636
  exports.blueprintShape = blueprintShape;
9462
9637
  exports.blueprintToMembers = blueprintToMembers;
@@ -9496,6 +9671,7 @@ exports.guideMemberTable = guideMemberTable;
9496
9671
  exports.guideMethods = guideMethods;
9497
9672
  exports.guideTests = guideTests;
9498
9673
  exports.guideUsage = guideUsage;
9674
+ exports.guidesViteProject = guidesViteProject;
9499
9675
  exports.hasBlueprintEnvironment = hasBlueprintEnvironment;
9500
9676
  exports.hasOnlyDataProperties = hasOnlyDataProperties;
9501
9677
  exports.hasValidArtifactBytes = hasValidArtifactBytes;
@@ -9509,6 +9685,7 @@ exports.hasValidSnapshotBytes = hasValidSnapshotBytes;
9509
9685
  exports.hasValidSyncReportBytes = hasValidSyncReportBytes;
9510
9686
  exports.hostGroup = hostGroup;
9511
9687
  exports.inferGroup = inferGroup;
9688
+ exports.integrationViteProject = integrationViteProject;
9512
9689
  exports.isArtifact = isArtifact;
9513
9690
  exports.isBehind = isBehind;
9514
9691
  exports.isBlueprint = isBlueprint;
@@ -9556,6 +9733,7 @@ exports.rootTsconfig = rootTsconfig;
9556
9733
  exports.rootViteConfig = rootViteConfig;
9557
9734
  exports.selectHostPaths = selectHostPaths;
9558
9735
  exports.serializeTypeScriptString = serializeTypeScriptString;
9736
+ exports.serviceViteProject = serviceViteProject;
9559
9737
  exports.singleSrcViteConfig = singleSrcViteConfig;
9560
9738
  exports.snapshotOf = snapshotOf;
9561
9739
  exports.snapshotPlan = snapshotPlan;
@@ -9573,5 +9751,7 @@ exports.validateDependencyArray = validateDependencyArray;
9573
9751
  exports.validatePlan = validatePlan;
9574
9752
  exports.viteHeader = viteHeader;
9575
9753
  exports.viteMachinery = viteMachinery;
9754
+ exports.viteProjectDefinitions = viteProjectDefinitions;
9755
+ exports.viteProjectRegistrations = viteProjectRegistrations;
9576
9756
 
9577
9757
  //# sourceMappingURL=index.cjs.map