@orkestrel/scaffold 0.0.27 → 0.0.29

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.
@@ -201,6 +201,12 @@ var GLOBAL_SETUP_PATH = "tests/setupGlobal.ts";
201
201
  var GUIDES_TEST_PATH = "tests/guides.test.ts";
202
202
  /** The installed-package proof whose presence makes a workspace `integration`. */
203
203
  var INTEGRATION_TEST_PATH = "tests/integration.test.ts";
204
+ /** The official-tooling drift proof whose presence makes a workspace `conformance`. */
205
+ var CONFORMANCE_TEST_PATH = "tests/conformance.test.ts";
206
+ /** The live-service readiness module whose presence makes a workspace `service`. */
207
+ var SERVICE_SETUP_PATH = "tests/setupService.ts";
208
+ /** The include the live-service project covers, which is a directory rather than one proof. */
209
+ var SERVICE_TEST_INCLUDE = "tests/service/**/*.test.ts";
204
210
  /** The Vite wrapper whose presence makes a workspace `showcase`. */
205
211
  var SHOWCASE_CONFIG_PATH = "configs/app/vite.showcase.config.ts";
206
212
  /** The bare workspace name syntax: lowercase alphanumeric with hyphens, letter first. */
@@ -284,7 +290,7 @@ var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
284
290
  var BASE_DEV_DEPENDENCIES = Object.freeze({
285
291
  "@microsoft/api-extractor": "^7.58.12",
286
292
  "@orkestrel/guide": "^0.0.10",
287
- "@orkestrel/scaffold": "^0.0.27",
293
+ "@orkestrel/scaffold": "^0.0.29",
288
294
  "@types/node": "^26.2.0",
289
295
  oxfmt: "^0.62.0",
290
296
  oxlint: "^1.77.0",
@@ -380,11 +386,11 @@ var CONFIG_TEMPLATES = Object.freeze({
380
386
  vite: `import type { {{viteTypes}} } from 'vite'
381
387
  {{imports}}import { defineConfig, mergeConfig } from 'vitest/config'
382
388
  import tsconfig from './tsconfig.json' with { type: 'json' }
383
- {{helpers}}import { lstatSync, readdirSync, realpathSync } from 'node:fs'
389
+ {{helpers}}{{browsers}}import { lstatSync, readdirSync, realpathSync } from 'node:fs'
384
390
  import { basename, join, parse, relative, resolve as resolvePath, sep } from 'node:path'
385
391
  import { fileURLToPath, URL } from 'node:url'
386
392
 
387
- export function resolveWorkspacePath(relativePath: string): string {
393
+ {{options}}export function resolveWorkspacePath(relativePath: string): string {
388
394
  return fileURLToPath(new URL(relativePath, import.meta.url))
389
395
  }
390
396
 
@@ -478,7 +484,7 @@ const resolve = {
478
484
  {{exclude}} setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
479
485
  browser: {
480
486
  enabled: true,
481
- provider: playwright(),
487
+ provider: playwright(browserOptions),
482
488
  instances: [{ browser: 'chromium', headless: true }],
483
489
  },
484
490
  fileParallelism: false,
@@ -589,7 +595,7 @@ const resolve = {
589
595
  setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
590
596
  browser: {
591
597
  enabled: true,
592
- provider: playwright(),
598
+ provider: playwright(browserOptions),
593
599
  instances: [{ browser: 'chromium', headless: true }],
594
600
  },
595
601
  fileParallelism: false,
@@ -676,6 +682,44 @@ export function appBrowser(...options: never[]): UserConfig {
676
682
  },
677
683
  options ?? {},
678
684
  )
685
+ `,
686
+ conformance: `// Where this package drifts from the official tooling it stays compatible with.
687
+ // The subject is this package, so the proof is hermetic and stays in \`npm test\`.
688
+ export const conformance = (options?: UserConfig): UserConfig =>
689
+ mergeConfig(
690
+ {
691
+ resolve,
692
+ test: {
693
+ name: { label: 'conformance', color: 'magenta' },
694
+ include: ['${CONFORMANCE_TEST_PATH}'],
695
+ setupFiles: ['./tests/setup.ts'],
696
+ environment: 'node',
697
+ browser: { enabled: false },
698
+ },
699
+ },
700
+ options ?? {},
701
+ )
702
+ `,
703
+ service: `// The live external services this package drives. It starts nothing itself:
704
+ // \`scripts/service.sh\` provisions, \`tests/setupService.ts\` proves readiness, and
705
+ // the project stays out of \`npm test\` because a real service answers it.
706
+ export const service = (options?: UserConfig): UserConfig =>
707
+ mergeConfig(
708
+ {
709
+ resolve,
710
+ test: {
711
+ name: { label: 'service', color: 'red' },
712
+ include: ['${SERVICE_TEST_INCLUDE}'],
713
+ setupFiles: ['./tests/setup.ts', './tests/setupService.ts'],
714
+ environment: 'node',
715
+ browser: { enabled: false },
716
+ testTimeout: 120_000,
717
+ hookTimeout: 120_000,
718
+ fileParallelism: false,
719
+ },
720
+ },
721
+ options ?? {},
722
+ )
679
723
  `,
680
724
  probe: `// A workbench, not a proof. No gate selects this project.
681
725
  export const probe = (options?: UserConfig): UserConfig =>
@@ -876,12 +920,22 @@ export default defineConfig(
876
920
  import dts from 'vite-plugin-dts'
877
921
  import { srcBrowser, resolveWorkspacePath } from '../../vite.config.ts'
878
922
 
923
+ // vite-plugin-dts rolls this face into one declaration, and the roll-up reaches
924
+ // src/core through a relative source path the tarball does not carry. The path
925
+ // keeps each source module's own depth, so a module in a browser subfolder emits
926
+ // one that leaves dist/src entirely. The rewrite below externalizes core through
927
+ // the package's own published root export, on the final roll-up only.
879
928
  export default defineConfig(
880
929
  srcBrowser({
881
930
  plugins: [
882
931
  dts({
883
932
  tsconfigPath: resolveWorkspacePath('configs/src/tsconfig.browser.json'),
884
933
  bundleTypes: true,
934
+ beforeWriteFile: (path, content) => ({
935
+ content: /[\\\\/]dist[\\\\/]src[\\\\/]browser[\\\\/]index\\.d\\.ts$/.test(path)
936
+ {{replacement}}
937
+ : content,
938
+ }),
885
939
  }),
886
940
  ],
887
941
  }),
@@ -950,7 +1004,319 @@ import { appShowcase } from '../../vite.config.ts'
950
1004
  export default defineConfig(appShowcase())
951
1005
  `
952
1006
  })
953
- })
1007
+ }),
1008
+ browsers: `// A generated browser workspace resolves its own Chromium here rather than in
1009
+ // \`configs/helpers.ts\`, because that leaf is vendored byte-identical to every
1010
+ // workspace and most of them declare no \`playwright\` to import.
1011
+
1012
+ import type { PlaywrightProviderOptions } from '@vitest/browser-playwright'
1013
+ import { chromium } from 'playwright'
1014
+ import { accessSync, constants as FS_CONSTANTS, globSync, readdirSync, statSync } from 'node:fs'
1015
+ import { basename, dirname, join, resolve as resolvePath } from 'node:path'
1016
+
1017
+ /**
1018
+ * Chromium executable layouts inside a \`chromium-<revision>\` browsers-directory entry, per
1019
+ * platform.
1020
+ *
1021
+ * @remarks
1022
+ * The current Playwright build ships Chrome for Testing on macOS. The trailing \`Chromium.app\`
1023
+ * layouts are what earlier builds shipped, so the list spans Playwright versions instead of
1024
+ * pinning to the installed one.
1025
+ */
1026
+ export const CHROMIUM_LAYOUTS = Object.freeze([
1027
+ 'chrome-linux/chrome',
1028
+ 'chrome-linux64/chrome',
1029
+ 'chrome-win/chrome.exe',
1030
+ 'chrome-win64/chrome.exe',
1031
+ 'chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
1032
+ 'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
1033
+ 'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
1034
+ 'chrome-mac-arm64/Chromium.app/Contents/MacOS/Chromium',
1035
+ ])
1036
+
1037
+ /** The \`chromium-<revision>\` entry name Playwright installs one managed build into. */
1038
+ export const CHROMIUM_ENTRY_PATTERN = /^chromium-\\d+$/
1039
+
1040
+ /** The revision number carried by any path containing a \`chromium-<revision>\` segment. */
1041
+ export const CHROMIUM_REVISION_PATTERN = /chromium-(\\d+)/
1042
+
1043
+ /** The directory a managed Linux container installs its bundled Playwright browsers into. */
1044
+ export const BUNDLED_BROWSERS_ROOT = '/opt/pw-browsers'
1045
+
1046
+ /**
1047
+ * Bundled Chromium layouts under the managed-container browsers root, as glob patterns.
1048
+ *
1049
+ * @remarks
1050
+ * The revision directory and its inner layout both drift across Playwright builds, and the
1051
+ * container also carries a top-level \`chromium\` alias, so every known shape is globbed.
1052
+ */
1053
+ export const BUNDLED_CHROMIUM_LAYOUTS = Object.freeze([
1054
+ 'chromium',
1055
+ 'chromium-*/chrome-linux64/chrome',
1056
+ 'chromium-*/chrome-linux/chrome',
1057
+ ])
1058
+
1059
+ /** Stable Playwright Chromium channels and their standard executable layouts. */
1060
+ export const SYSTEM_BROWSER_CHANNELS = Object.freeze([
1061
+ Object.freeze({
1062
+ channel: 'chrome',
1063
+ layouts: Object.freeze({
1064
+ linux: '/opt/google/chrome/chrome',
1065
+ darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
1066
+ win32: Object.freeze(['Google', 'Chrome', 'Application', 'chrome.exe']),
1067
+ }),
1068
+ }),
1069
+ Object.freeze({
1070
+ channel: 'msedge',
1071
+ layouts: Object.freeze({
1072
+ linux: '/opt/microsoft/msedge/msedge',
1073
+ darwin: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
1074
+ win32: Object.freeze(['Microsoft', 'Edge', 'Application', 'msedge.exe']),
1075
+ }),
1076
+ }),
1077
+ ])
1078
+
1079
+ /**
1080
+ * Determine whether a path identifies an executable regular file.
1081
+ *
1082
+ * @param path - The filesystem path to inspect.
1083
+ * @returns Whether the path is a regular file with execute access.
1084
+ *
1085
+ * @example
1086
+ * \`\`\`ts
1087
+ * isBrowserExecutable('/opt/google/chrome/chrome')
1088
+ * \`\`\`
1089
+ */
1090
+ export function isBrowserExecutable(path: string): boolean {
1091
+ try {
1092
+ if (!statSync(path).isFile()) return false
1093
+ accessSync(path, FS_CONSTANTS.X_OK)
1094
+ return true
1095
+ } catch {
1096
+ return false
1097
+ }
1098
+ }
1099
+
1100
+ /**
1101
+ * Order two Chromium paths so the highest revision sorts first.
1102
+ *
1103
+ * @param left - The first path or directory entry to compare.
1104
+ * @param right - The second path or directory entry to compare.
1105
+ * @returns A negative number when \`left\` sorts first, positive when \`right\` does.
1106
+ *
1107
+ * @remarks
1108
+ * Revisions are numbers, so \`chromium-1200\` outranks \`chromium-999\` despite sorting below it
1109
+ * lexically. A path carrying no revision falls back to descending name order.
1110
+ *
1111
+ * @example
1112
+ * \`\`\`ts
1113
+ * ['chromium-999', 'chromium-1200'].sort(compareRevisions)
1114
+ * \`\`\`
1115
+ */
1116
+ export function compareRevisions(left: string, right: string): number {
1117
+ const leftRevision = CHROMIUM_REVISION_PATTERN.exec(left)?.[1]
1118
+ const rightRevision = CHROMIUM_REVISION_PATTERN.exec(right)?.[1]
1119
+ if (leftRevision === undefined || rightRevision === undefined) return right.localeCompare(left)
1120
+ return Number(rightRevision) - Number(leftRevision)
1121
+ }
1122
+
1123
+ /**
1124
+ * Read the executable path of Playwright's pinned Chromium revision.
1125
+ *
1126
+ * @returns The pinned executable path, or \`undefined\` when this platform has none.
1127
+ *
1128
+ * @remarks
1129
+ * Playwright throws rather than returning a path when the current platform carries no initialized
1130
+ * executable, and an unguarded call would fail configuration evaluation for every project.
1131
+ *
1132
+ * @example
1133
+ * \`\`\`ts
1134
+ * resolvePinnedBrowser()
1135
+ * \`\`\`
1136
+ */
1137
+ export function resolvePinnedBrowser(): string | undefined {
1138
+ try {
1139
+ const pinned = chromium.executablePath()
1140
+ return pinned.length === 0 ? undefined : pinned
1141
+ } catch {
1142
+ return undefined
1143
+ }
1144
+ }
1145
+
1146
+ /**
1147
+ * Resolve a launchable Playwright-managed Chromium executable: the pinned revision when installed,
1148
+ * otherwise a \`chromium\` / \`chromium.exe\` alias or any other \`chromium-*\` revision under the same
1149
+ * Playwright browsers directory. A pinned-revision miss is not Chromium absence — managed
1150
+ * containers ship one usable build, often behind a revision-agnostic alias, for many Playwright
1151
+ * versions.
1152
+ *
1153
+ * @param pinned - The executable path for Playwright's pinned Chromium revision.
1154
+ * @returns The managed executable path, or \`undefined\` when none is executable.
1155
+ *
1156
+ * @example
1157
+ * \`\`\`ts
1158
+ * resolveManagedBrowser('/root/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome')
1159
+ * \`\`\`
1160
+ */
1161
+ export function resolveManagedBrowser(pinned: string): string | undefined {
1162
+ if (isBrowserExecutable(pinned)) return pinned
1163
+ let revisionRoot = dirname(pinned)
1164
+ for (;;) {
1165
+ if (CHROMIUM_ENTRY_PATTERN.test(basename(revisionRoot))) break
1166
+ const parent = dirname(revisionRoot)
1167
+ if (parent === revisionRoot) return undefined
1168
+ revisionRoot = parent
1169
+ }
1170
+ const browsersRoot = dirname(revisionRoot)
1171
+ for (const alias of ['chromium', 'chromium.exe']) {
1172
+ const candidate = resolvePath(browsersRoot, alias)
1173
+ if (isBrowserExecutable(candidate)) return candidate
1174
+ }
1175
+ let entries: readonly string[]
1176
+ try {
1177
+ entries = readdirSync(browsersRoot)
1178
+ } catch {
1179
+ return undefined
1180
+ }
1181
+ const revisions = entries
1182
+ .filter((entry) => CHROMIUM_ENTRY_PATTERN.test(entry))
1183
+ .sort(compareRevisions)
1184
+ for (const revision of revisions) {
1185
+ for (const layout of CHROMIUM_LAYOUTS) {
1186
+ const candidate = resolvePath(browsersRoot, revision, layout)
1187
+ if (isBrowserExecutable(candidate)) return candidate
1188
+ }
1189
+ }
1190
+ return undefined
1191
+ }
1192
+
1193
+ /**
1194
+ * Resolve the Chromium a managed Linux container bundles outside the Playwright cache.
1195
+ *
1196
+ * @param platform - The Node platform the container runs on.
1197
+ * @param root - The bundled browsers directory to search.
1198
+ * @returns The highest matching executable path, or \`undefined\` when none is executable.
1199
+ *
1200
+ * @example
1201
+ * \`\`\`ts
1202
+ * resolveBundledBrowser('linux', BUNDLED_BROWSERS_ROOT)
1203
+ * \`\`\`
1204
+ */
1205
+ export function resolveBundledBrowser(platform: NodeJS.Platform, root: string): string | undefined {
1206
+ if (platform !== 'linux') return undefined
1207
+ for (const layout of BUNDLED_CHROMIUM_LAYOUTS) {
1208
+ let matches: readonly string[]
1209
+ try {
1210
+ matches = globSync(layout, { cwd: root })
1211
+ } catch {
1212
+ return undefined
1213
+ }
1214
+ for (const match of [...matches].sort(compareRevisions)) {
1215
+ const candidate = resolvePath(root, match)
1216
+ if (isBrowserExecutable(candidate)) return candidate
1217
+ }
1218
+ }
1219
+ return undefined
1220
+ }
1221
+
1222
+ /**
1223
+ * Resolve the first installed stable system Chromium channel.
1224
+ *
1225
+ * @param platform - The Node platform whose standard layouts should be probed.
1226
+ * @param environment - The process environment supplying Windows installation roots.
1227
+ * @returns \`chrome\`, then \`msedge\`, or \`undefined\` when neither is executable.
1228
+ *
1229
+ * @example
1230
+ * \`\`\`ts
1231
+ * resolveSystemBrowser(process.platform, process.env)
1232
+ * \`\`\`
1233
+ */
1234
+ export function resolveSystemBrowser(
1235
+ platform: NodeJS.Platform,
1236
+ environment: NodeJS.ProcessEnv,
1237
+ ): string | undefined {
1238
+ if (platform !== 'linux' && platform !== 'darwin' && platform !== 'win32') return undefined
1239
+ const roots = new Set<string>()
1240
+ if (platform === 'win32') {
1241
+ for (const root of [
1242
+ environment.LOCALAPPDATA,
1243
+ environment.PROGRAMFILES,
1244
+ environment['PROGRAMFILES(X86)'],
1245
+ ]) {
1246
+ if (root !== undefined && root.length > 0) roots.add(root)
1247
+ }
1248
+ const homeDrive = environment.HOMEDRIVE
1249
+ if (homeDrive !== undefined && homeDrive.length > 0) {
1250
+ roots.add(join(homeDrive, 'Program Files'))
1251
+ roots.add(join(homeDrive, 'Program Files (x86)'))
1252
+ }
1253
+ }
1254
+ for (const browser of SYSTEM_BROWSER_CHANNELS) {
1255
+ if (platform === 'win32') {
1256
+ for (const root of roots) {
1257
+ if (isBrowserExecutable(join(root, ...browser.layouts.win32))) return browser.channel
1258
+ }
1259
+ continue
1260
+ }
1261
+ if (isBrowserExecutable(browser.layouts[platform])) return browser.channel
1262
+ }
1263
+ return undefined
1264
+ }
1265
+
1266
+ /**
1267
+ * Resolve Playwright provider options for whatever browser this host can actually launch.
1268
+ *
1269
+ * @param pinned - The executable path for Playwright's pinned Chromium revision, when it has one.
1270
+ * @param platform - The Node platform whose standard layouts should be probed.
1271
+ * @param environment - The process environment supplying operator overrides and Windows roots.
1272
+ * @param root - The managed-container bundled browsers directory to search.
1273
+ * @returns Provider options naming an executable, a WebSocket endpoint, or a channel.
1274
+ *
1275
+ * @remarks
1276
+ * Precedence, most important first: \`PLAYWRIGHT_EXECUTABLE_PATH\`, \`PLAYWRIGHT_WS_ENDPOINT\`,
1277
+ * \`PLAYWRIGHT_CHANNEL\`, the managed Playwright Chromium, the container's bundled Chromium, a
1278
+ * verified system channel, then the platform default channel. An operator override outranks
1279
+ * discovery and is returned exactly as given: none of those three environment values is checked
1280
+ * against the filesystem, because verifying an override would defeat the override. The pinned
1281
+ * managed revision outranks anything found on the host because it is deterministic. The installed
1282
+ * pinned revision returns empty options so Playwright keeps its own default launch semantics. Only
1283
+ * a discovered system channel is verified before it is named. The platform default is unverified
1284
+ * as well and exists only as a last resort: Windows takes \`msedge\`, which ships with the OS and
1285
+ * never collides with a foreground Chrome.
1286
+ *
1287
+ * @example
1288
+ * \`\`\`ts
1289
+ * resolveBrowser(resolvePinnedBrowser(), process.platform, process.env)
1290
+ * \`\`\`
1291
+ */
1292
+ export function resolveBrowser(
1293
+ pinned: string | undefined,
1294
+ platform: NodeJS.Platform,
1295
+ environment: NodeJS.ProcessEnv,
1296
+ root: string = BUNDLED_BROWSERS_ROOT,
1297
+ ): PlaywrightProviderOptions {
1298
+ const executable = environment.PLAYWRIGHT_EXECUTABLE_PATH
1299
+ if (executable !== undefined && executable.length > 0) {
1300
+ return { launchOptions: { executablePath: executable } }
1301
+ }
1302
+ const endpoint = environment.PLAYWRIGHT_WS_ENDPOINT
1303
+ if (endpoint !== undefined && endpoint.length > 0) {
1304
+ return { connectOptions: { wsEndpoint: endpoint } }
1305
+ }
1306
+ const requested = environment.PLAYWRIGHT_CHANNEL
1307
+ if (requested !== undefined && requested.length > 0) {
1308
+ return { launchOptions: { channel: requested } }
1309
+ }
1310
+ const managed = pinned === undefined ? undefined : resolveManagedBrowser(pinned)
1311
+ if (managed !== undefined) {
1312
+ return managed === pinned ? {} : { launchOptions: { executablePath: managed } }
1313
+ }
1314
+ const bundled = resolveBundledBrowser(platform, root)
1315
+ if (bundled !== undefined) return { launchOptions: { executablePath: bundled } }
1316
+ const fallback = platform === 'win32' ? 'msedge' : 'chrome'
1317
+ return { launchOptions: { channel: resolveSystemBrowser(platform, environment) ?? fallback } }
1318
+ }
1319
+ `
954
1320
  });
955
1321
  /**
956
1322
  * Formatter-stable template text for source, test, document, guide, and service artifacts.
@@ -1150,7 +1516,7 @@ npm test
1150
1516
  set -eu
1151
1517
 
1152
1518
  printf '%s\\n' \\
1153
- {{services}}
1519
+ {{vendors}}
1154
1520
  ` })
1155
1521
  });
1156
1522
  //#endregion
@@ -1437,7 +1803,9 @@ var isBlueprint = (0, _orkestrel_contract.recordOf)({
1437
1803
  overrides: (0, _orkestrel_contract.andOf)(isCollection, (0, _orkestrel_contract.arrayOf)(isOverride)),
1438
1804
  bin: _orkestrel_contract.isBoolean,
1439
1805
  integration: _orkestrel_contract.isBoolean,
1440
- services: (0, _orkestrel_contract.andOf)(isCollection, (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)),
1806
+ conformance: _orkestrel_contract.isBoolean,
1807
+ service: _orkestrel_contract.isBoolean,
1808
+ vendors: (0, _orkestrel_contract.andOf)(isCollection, (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)),
1441
1809
  global: _orkestrel_contract.isBoolean,
1442
1810
  showcase: _orkestrel_contract.isBoolean
1443
1811
  }, ["description"]);
@@ -2029,6 +2397,46 @@ function nameToGuide(name) {
2029
2397
  return `guides/${name.slice(name.lastIndexOf("/") + 1)}.md`;
2030
2398
  }
2031
2399
  /**
2400
+ * Derive the declaration rewrite a published face's `beforeWriteFile` applies.
2401
+ *
2402
+ * @param name - The workspace's own bare package name.
2403
+ * @returns The ternary consequent an emitted `vite.{browser,server}.config.ts`
2404
+ * fills its `{{replacement}}` span with, indented for that span.
2405
+ *
2406
+ * @remarks
2407
+ * `vite-plugin-dts` rolls a face into one declaration and keeps each source
2408
+ * module's own relative depth, so a nested module emits a path that escapes
2409
+ * `dist/src` and a flat one resolves only by luck. Both faces rewrite the same
2410
+ * relative core path to the package's published root export, so the branch is
2411
+ * derived once here. The extension alternation is what the two permitted import
2412
+ * spellings produce: an `@src/core` alias resolves to the core source module and
2413
+ * prints `.ts`, while a relative import prints the `.js` specifier it was
2414
+ * written with. The formatter keeps the call on one line only while the line it
2415
+ * prints measures inside the vendored width, and the workspace name is what
2416
+ * varies, so the shape is chosen by measuring the candidate: a tab prints as the
2417
+ * vendored two columns, and the gate admits a name long enough to push the
2418
+ * joined call past 100.
2419
+ *
2420
+ * @example
2421
+ * ```ts
2422
+ * import { nameToRewrite } from '@orkestrel/scaffold'
2423
+ *
2424
+ * nameToRewrite('router').includes("'@orkestrel/router'") // true
2425
+ * ```
2426
+ */
2427
+ function nameToRewrite(name) {
2428
+ const specifier = serializeTypeScriptString(`@orkestrel/${name}`);
2429
+ const pattern = "/(?:\\.\\.\\/)+core\\/index\\.[jt]s/g";
2430
+ const joined = `\t\t\t\t\t\t? content.replaceAll(${pattern}, ${specifier})`;
2431
+ if (joined.replaceAll(" ", " ").length <= 100) return joined;
2432
+ return [
2433
+ " ? content.replaceAll(",
2434
+ `\t\t\t\t\t\t\t\t${pattern},`,
2435
+ `\t\t\t\t\t\t\t\t${specifier},`,
2436
+ " )"
2437
+ ].join("\n");
2438
+ }
2439
+ /**
2032
2440
  * Select the host paths a named workspace vendors.
2033
2441
  *
2034
2442
  * @param paths - The candidate host paths, in their declared order.
@@ -2673,8 +3081,14 @@ function blueprintToDevDependencies(blueprint) {
2673
3081
  * to the axes the blueprint declares: a check and a test script per declared
2674
3082
  * environment, an aggregate over each axis, the policy and configuration
2675
3083
  * proofs every workspace can pass before it has a public API, and one build per
2676
- * target that actually builds. The isolated installed-package integration
2677
- * proof stays out of `test` and runs from `prepublishOnly` instead.
3084
+ * target that actually builds.
3085
+ *
3086
+ * A proof leaves `test` when a real service or a real install answers it. The
3087
+ * installed-package integration proof and the live-service proof therefore run
3088
+ * from `prepublishOnly` instead. The conformance proof stays in `test`, because
3089
+ * it measures this package against official tooling and drives nothing external:
3090
+ * a conformance run may start a server, but it starts its own and reaches it
3091
+ * over loopback, so the run stays hermetic.
2678
3092
  *
2679
3093
  * The configuration paths interpolated here are the same ones `SRC_MATRIX` and
2680
3094
  * `APP_MATRIX` list as each environment's configuration files, so a rename in
@@ -2721,7 +3135,8 @@ function blueprintToScripts(blueprint) {
2721
3135
  ...compiles ? ["npm run test:src"] : [],
2722
3136
  ...blueprint.app.length > 0 ? ["npm run test:app"] : [],
2723
3137
  "npm run test:policy",
2724
- "npm run test:config"
3138
+ "npm run test:config",
3139
+ ...blueprint.conformance ? ["npm run test:conformance"] : []
2725
3140
  ].join(" && ");
2726
3141
  if (compiles) {
2727
3142
  scripts["test:src"] = [
@@ -2738,8 +3153,10 @@ function blueprintToScripts(blueprint) {
2738
3153
  }
2739
3154
  scripts["test:policy"] = `${vitest} --project policy`;
2740
3155
  scripts["test:config"] = `${vitest} --project config`;
3156
+ if (blueprint.conformance) scripts["test:conformance"] = `${vitest} --project conformance`;
2741
3157
  scripts["test:probe"] = "vitest run --config vite.config.ts --no-cache --reporter=verbose --project probe";
2742
3158
  if (integrates) scripts["test:integration"] = `${vitest} --project integration`;
3159
+ if (blueprint.service) scripts["test:service"] = `${vitest} --project service`;
2743
3160
  scripts.build = [
2744
3161
  "npm run clean",
2745
3162
  ...compiles ? ["npm run build:src"] : [],
@@ -2769,7 +3186,11 @@ function blueprintToScripts(blueprint) {
2769
3186
  scripts.serve = "node dist/app/server/main.cjs";
2770
3187
  scripts["serve:build"] = "npm run build:app:server && npm run serve";
2771
3188
  }
2772
- scripts.prepublishOnly = ["npm run format:check && npm run lint:check && npm run check && npm run build && npm test", ...integrates ? ["npm run test:integration"] : []].join(" && ");
3189
+ scripts.prepublishOnly = [
3190
+ "npm run format:check && npm run lint:check && npm run check && npm run build && npm test",
3191
+ ...integrates ? ["npm run test:integration"] : [],
3192
+ ...blueprint.service ? ["npm run test:service"] : []
3193
+ ].join(" && ");
2773
3194
  return scripts;
2774
3195
  }
2775
3196
  /**
@@ -3051,6 +3472,14 @@ export function appShowcase(...options: never[]): UserConfig {
3051
3472
  projects.push("config");
3052
3473
  factories.push(CONFIG_TEMPLATES.factories.guides);
3053
3474
  projects.push(`...(isExactCaseFile(resolveWorkspacePath('${GUIDES_TEST_PATH}')) ? [guides] : [])`);
3475
+ if (blueprint.conformance) {
3476
+ factories.push(CONFIG_TEMPLATES.factories.conformance);
3477
+ projects.push("conformance");
3478
+ }
3479
+ if (blueprint.service) {
3480
+ factories.push(CONFIG_TEMPLATES.factories.service);
3481
+ projects.push("service");
3482
+ }
3054
3483
  if (blueprint.src.length > 0 && blueprint.integration) {
3055
3484
  factories.push((0, _orkestrel_template.fillTemplate)(CONFIG_TEMPLATES.factories.integration, { global: blueprint.global ? " globalSetup: ['./tests/setupGlobal.ts'],\n" : "" }));
3056
3485
  projects.push("integration");
@@ -3066,6 +3495,8 @@ ${projects.map((project) => `\t\t\t${project},`).join("\n")}
3066
3495
  viteTypes: machinery.showcase ? "PluginOption, UserConfig" : "UserConfig",
3067
3496
  imports: imports.length === 0 ? "" : `${imports.join("\n")}\n`,
3068
3497
  helpers: boundaries.length === 0 ? "" : `import { ${boundaries.join(", ")} } from './configs/helpers.js'\n`,
3498
+ browsers: machinery.browser ? "import { resolveBrowser, resolvePinnedBrowser } from './configs/browsers.js'\n" : "",
3499
+ options: machinery.browser ? "const browserOptions = resolveBrowser(resolvePinnedBrowser(), process.platform, process.env)\n\n" : "",
3069
3500
  factories: body,
3070
3501
  projects: projectRows
3071
3502
  });
@@ -3099,22 +3530,20 @@ function blueprintToConfigArtifacts(blueprint) {
3099
3530
  origin: "template",
3100
3531
  content: blueprintToRootVite(blueprint)
3101
3532
  }];
3533
+ if (blueprintToMachinery(blueprint).browser) artifacts.push({
3534
+ path: "configs/browsers.ts",
3535
+ group: "configs",
3536
+ ownership: "content",
3537
+ origin: "template",
3538
+ content: CONFIG_TEMPLATES.browsers
3539
+ });
3102
3540
  for (const environment of blueprint.src) for (const path of SRC_MATRIX[environment].configs) {
3103
3541
  let content = CONFIG_TEMPLATES.vites.src.core;
3104
3542
  if (path === "configs/src/tsconfig.core.json") content = CONFIG_TEMPLATES.tsconfigs.src.core;
3105
- else if (path === "configs/src/vite.browser.config.ts") content = CONFIG_TEMPLATES.vites.src.browser;
3543
+ else if (path === "configs/src/vite.browser.config.ts") content = (0, _orkestrel_template.fillTemplate)(CONFIG_TEMPLATES.vites.src.browser, { replacement: nameToRewrite(blueprint.name) });
3106
3544
  else if (path === "configs/src/tsconfig.browser.json") content = CONFIG_TEMPLATES.tsconfigs.src.browser;
3107
- else if (path === "configs/src/vite.server.config.ts") {
3108
- const packageName = serializeTypeScriptString(`@orkestrel/${blueprint.name}`);
3109
- const joined = `\t\t\t\t\t\t? content.replaceAll(/(?:\\.\\.\\/)+core\\/index\\.ts/g, ${packageName})`;
3110
- const replacement = joined.replaceAll(" ", " ").length <= 100 ? joined : [
3111
- " ? content.replaceAll(",
3112
- " /(?:\\.\\.\\/)+core\\/index\\.ts/g,",
3113
- `\t\t\t\t\t\t\t\t${packageName},`,
3114
- " )"
3115
- ].join("\n");
3116
- content = (0, _orkestrel_template.fillTemplate)(CONFIG_TEMPLATES.vites.src.server, { replacement });
3117
- } else if (path === "configs/src/tsconfig.server.json") content = CONFIG_TEMPLATES.tsconfigs.src.server;
3545
+ else if (path === "configs/src/vite.server.config.ts") content = (0, _orkestrel_template.fillTemplate)(CONFIG_TEMPLATES.vites.src.server, { replacement: nameToRewrite(blueprint.name) });
3546
+ else if (path === "configs/src/tsconfig.server.json") content = CONFIG_TEMPLATES.tsconfigs.src.server;
3118
3547
  artifacts.push({
3119
3548
  path,
3120
3549
  group: "configs",
@@ -3271,6 +3700,12 @@ function blueprintToSourceArtifacts(blueprint) {
3271
3700
  * workspace and therefore follows the `src` axis as well as its structural
3272
3701
  * flag.
3273
3702
  *
3703
+ * The conformance proof is not emitted either, for the reason the guide proof
3704
+ * is not: it names an official artifact only the package knows, so a generated
3705
+ * placeholder would read as a proof while measuring nothing. `service` emits its
3706
+ * readiness setup alone, because the root configuration names that module by
3707
+ * path.
3708
+ *
3274
3709
  * @example
3275
3710
  * ```ts
3276
3711
  * import { blueprintToTestArtifacts, createBlueprint } from '@orkestrel/scaffold'
@@ -3304,6 +3739,13 @@ function blueprintToTestArtifacts(blueprint) {
3304
3739
  environment: "server",
3305
3740
  content: ARTIFACT_TEMPLATES.tests.setup
3306
3741
  });
3742
+ if (blueprint.service) artifacts.push({
3743
+ path: SERVICE_SETUP_PATH,
3744
+ group: "tests",
3745
+ ownership: "birth",
3746
+ origin: "template",
3747
+ content: ARTIFACT_TEMPLATES.tests.setup
3748
+ });
3307
3749
  if (blueprint.global) artifacts.push({
3308
3750
  path: GLOBAL_SETUP_PATH,
3309
3751
  group: "tests",
@@ -3417,22 +3859,22 @@ function blueprintToDocumentArtifacts(blueprint) {
3417
3859
  * Compile the blueprint-dependent orchestration artifacts.
3418
3860
  *
3419
3861
  * @param blueprint - The workspace specification.
3420
- * @returns A service inventory script when services are declared, otherwise none.
3862
+ * @returns A vendor inventory script when vendors are declared, otherwise none.
3421
3863
  *
3422
3864
  * @remarks
3423
- * A service name does not describe startup, readiness, or cleanup. The script
3865
+ * A vendor name does not describe startup, readiness, or cleanup. The script
3424
3866
  * therefore records only the declared inventory and does not invent a service
3425
3867
  * runner or test project.
3426
3868
  */
3427
3869
  function blueprintToOrchestrationArtifacts(blueprint) {
3428
- if (blueprint.services.length === 0) return [];
3429
- const services = blueprint.services.map((service, index) => `\t'${service}'${index === blueprint.services.length - 1 ? "" : " \\"}`).join("\n");
3870
+ if (blueprint.vendors.length === 0) return [];
3871
+ const vendors = blueprint.vendors.map((vendor, index) => `\t'${vendor}'${index === blueprint.vendors.length - 1 ? "" : " \\"}`).join("\n");
3430
3872
  return [{
3431
3873
  path: SERVICE_SCRIPT_PATH,
3432
3874
  group: "orchestration",
3433
3875
  ownership: "birth",
3434
3876
  origin: "template",
3435
- content: (0, _orkestrel_template.fillTemplate)(ARTIFACT_TEMPLATES.orchestration.service, { services })
3877
+ content: (0, _orkestrel_template.fillTemplate)(ARTIFACT_TEMPLATES.orchestration.service, { vendors })
3436
3878
  }];
3437
3879
  }
3438
3880
  /**
@@ -3788,19 +4230,19 @@ function blueprintToQuestions(blueprint) {
3788
4230
  message: "integration projects a published src, and this workspace declares none, so it emits nothing.",
3789
4231
  blocking: false
3790
4232
  });
3791
- const services = /* @__PURE__ */ new Set();
3792
- for (const service of blueprint.services) {
3793
- if (!NAME_PATTERN.test(service)) questions.push({
3794
- field: "services",
3795
- message: `${service} is not a lowercase alphanumeric service name starting with a letter.`,
4233
+ const vendors = /* @__PURE__ */ new Set();
4234
+ for (const vendor of blueprint.vendors) {
4235
+ if (!NAME_PATTERN.test(vendor)) questions.push({
4236
+ field: "vendors",
4237
+ message: `${vendor} is not a lowercase alphanumeric vendor name starting with a letter.`,
3796
4238
  blocking: true
3797
4239
  });
3798
- else if (services.has(service)) questions.push({
3799
- field: "services",
3800
- message: `${service} is declared more than once on services.`,
4240
+ else if (vendors.has(vendor)) questions.push({
4241
+ field: "vendors",
4242
+ message: `${vendor} is declared more than once on vendors.`,
3801
4243
  blocking: true
3802
4244
  });
3803
- services.add(service);
4245
+ vendors.add(vendor);
3804
4246
  }
3805
4247
  if (blueprint.showcase && !blueprint.app.includes("browser")) questions.push({
3806
4248
  field: "showcase",
@@ -4268,8 +4710,8 @@ var Compiler = class {
4268
4710
  * blueprint.
4269
4711
  *
4270
4712
  * @remarks
4271
- * A blueprint is a closed record of sixteen fields, and most of them have one
4272
- * sensible starting value: an empty list, a cleared flag, `DEFAULT_VERSION`, and
4713
+ * A blueprint is a closed record, and most of its fields have one sensible
4714
+ * starting value: an empty list, a cleared flag, `DEFAULT_VERSION`, and
4273
4715
  * `DEFAULT_ENGINES`. Filling them here is what lets a caller state only what its
4274
4716
  * workspace actually declares.
4275
4717
  *
@@ -4310,7 +4752,9 @@ function createBlueprint(name, input) {
4310
4752
  overrides: input?.overrides ?? [],
4311
4753
  bin: input?.bin ?? false,
4312
4754
  integration: input?.integration ?? false,
4313
- services: input?.services ?? [],
4755
+ conformance: input?.conformance ?? false,
4756
+ service: input?.service ?? false,
4757
+ vendors: input?.vendors ?? [],
4314
4758
  global: input?.global ?? false,
4315
4759
  showcase: input?.showcase ?? false
4316
4760
  }));
@@ -4347,6 +4791,7 @@ exports.BIN_CONFIGS = BIN_CONFIGS;
4347
4791
  exports.BIN_ENTRY_PATH = BIN_ENTRY_PATH;
4348
4792
  exports.CATALOG_AGENT_PATH = CATALOG_AGENT_PATH;
4349
4793
  exports.CONFIG_TEMPLATES = CONFIG_TEMPLATES;
4794
+ exports.CONFORMANCE_TEST_PATH = CONFORMANCE_TEST_PATH;
4350
4795
  exports.CONTROL_CHARACTER_PATTERN = CONTROL_CHARACTER_PATTERN;
4351
4796
  exports.Compiler = Compiler;
4352
4797
  exports.DEFAULT_ENGINES = DEFAULT_ENGINES;
@@ -4380,6 +4825,8 @@ exports.ORCHESTRATION_PATH_NAMES = ORCHESTRATION_PATH_NAMES;
4380
4825
  exports.ORCHESTRATION_PATH_PREFIXES = ORCHESTRATION_PATH_PREFIXES;
4381
4826
  exports.ORKESTREL_RANGE_PATTERN = ORKESTREL_RANGE_PATTERN;
4382
4827
  exports.SERVICE_SCRIPT_PATH = SERVICE_SCRIPT_PATH;
4828
+ exports.SERVICE_SETUP_PATH = SERVICE_SETUP_PATH;
4829
+ exports.SERVICE_TEST_INCLUDE = SERVICE_TEST_INCLUDE;
4383
4830
  exports.SHOWCASE_CONFIG_PATH = SHOWCASE_CONFIG_PATH;
4384
4831
  exports.SHOWCASE_DEV_DEPENDENCIES = SHOWCASE_DEV_DEPENDENCIES;
4385
4832
  exports.SOURCE_BROWSER_DEV_DEPENDENCIES = SOURCE_BROWSER_DEV_DEPENDENCIES;
@@ -4446,6 +4893,7 @@ exports.matchesOrchestrationPath = matchesOrchestrationPath;
4446
4893
  exports.matchesRange = matchesRange;
4447
4894
  exports.nameToGuide = nameToGuide;
4448
4895
  exports.nameToHostArtifacts = nameToHostArtifacts;
4896
+ exports.nameToRewrite = nameToRewrite;
4449
4897
  exports.overridesToQuestions = overridesToQuestions;
4450
4898
  exports.parseBlueprint = parseBlueprint;
4451
4899
  exports.parseCompilerOptions = parseCompilerOptions;