@orkestrel/scaffold 0.0.101 → 0.0.102

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.
Files changed (58) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
  7. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
  8. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
  9. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
  10. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
  12. package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
  13. package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
  14. package/dist/bin/main.js +483 -444
  15. package/dist/bin/main.js.map +1 -1
  16. package/dist/host/AGENTS.md +1 -0
  17. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
  18. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
  19. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
  20. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
  21. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
  22. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
  23. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
  24. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
  25. package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
  26. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
  27. package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
  28. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
  29. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
  30. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
  31. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
  32. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
  33. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
  34. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
  35. package/dist/host/claude/rules/architecture.md +2 -2
  36. package/dist/host/claude/rules/tests.md +8 -2
  37. package/dist/host/claude/rules/typescript.md +130 -0
  38. package/dist/host/claude/rules/workspace.md +30 -12
  39. package/dist/host/configs/helpers.ts +206 -133
  40. package/dist/host/configs/policy.ts +110 -105
  41. package/dist/host/guides/scaffold.md +244 -170
  42. package/dist/host/manifest.json +35 -29
  43. package/dist/host/tests/config.test.ts +1220 -633
  44. package/dist/host/tests/policy.test.ts +19 -0
  45. package/dist/host/tests/setupPolicy.ts +1669 -985
  46. package/dist/src/core/index.cjs +1595 -1686
  47. package/dist/src/core/index.cjs.map +1 -1
  48. package/dist/src/core/index.d.cts +202 -18
  49. package/dist/src/core/index.d.ts +202 -18
  50. package/dist/src/core/index.js +1590 -1687
  51. package/dist/src/core/index.js.map +1 -1
  52. package/dist/src/server/index.cjs +448 -392
  53. package/dist/src/server/index.cjs.map +1 -1
  54. package/dist/src/server/index.d.cts +1182 -1052
  55. package/dist/src/server/index.d.ts +1182 -1052
  56. package/dist/src/server/index.js +447 -395
  57. package/dist/src/server/index.js.map +1 -1
  58. package/package.json +2 -2
@@ -68,12 +68,19 @@ Give every selected environment and every face an alias: `src/styles`, each `src
68
68
  - `configs/agents/tsconfig.skills.json`: the scoped typecheck of the skill scripts; its presence selects the `skills` blueprint fact.
69
69
  - `configs/src/` and `configs/app/`: thin per-target wrappers, including optional
70
70
  `configs/src/*bin*` files. Shared logic remains in root configs.
71
- - `configs/helpers.ts`, `configs/browsers.ts`, and `configs/policy.ts`: the only permitted leaves
72
- under `configs/`. Each imports nothing from the workspace, which is what keeps it a leaf, so no
73
- `configs/types.ts` exists for one to import: each keeps its own types, data, and functions in its
74
- one file, and the centralized-kind placement in `.claude/rules/architecture.md` does not reach a
75
- leaf. Each `configs/src/*.config.ts` wrapper imports the root config and may import the permitted
76
- leaves; keep shared build and project composition in the root config.
71
+ - `configs/helpers.ts`, `configs/browsers.ts`, `configs/commands.ts`, and `configs/policy.ts`: the
72
+ only permitted leaves under `configs/`. Each imports nothing from the workspace, which is what
73
+ keeps it a leaf, so no `configs/types.ts` exists for one to import: each keeps its own types,
74
+ data, and functions in its one file, and the centralized-kind placement in
75
+ `.claude/rules/architecture.md` does not reach a leaf. Each `configs/src/*.config.ts` wrapper
76
+ imports the root config and may import the permitted leaves; keep shared build and project
77
+ composition in the root config.
78
+ - Register the package's own Vitest browser commands in the `commands` record of
79
+ `configs/commands.ts`, the presence-owned seed scaffold plans beside `configs/browsers.ts`.
80
+ Import only a `node:` module or a package the manifest declares in `dependencies` or
81
+ `devDependencies` there: the root config bundles the file into every config load, builds
82
+ included, and the bundle resolves no workspace alias. Load workspace source inside a command
83
+ body through `context.project.vitest.import`, never through a static import.
77
84
  - Keep `configs/helpers.ts` free of any dependency a core-only workspace does not declare. It is
78
85
  vendored byte-identical to every workspace, so an import there must resolve in all of them.
79
86
  `configs/browsers.ts` exists for that reason: it imports `playwright` and
@@ -148,6 +155,9 @@ environment axis is one project per src/app axis × environment, plus one per ex
148
155
  - Give every browser project `optimizeDeps.include` of `@orkestrel/test`, `@orkestrel/test/browser`,
149
156
  `@orkestrel/contract` where the manifest declares it, and `vue` where the project renders Vue.
150
157
  Give a Node project no `optimizeDeps` setting.
158
+ - Give every browser project `test.browser.commands` from `configs/commands.ts`: each root factory
159
+ that names the Playwright provider reads the record, and a project that spreads another
160
+ factory's `test.browser`, as `appVue` and every `journey:<variant>` project do, keeps it.
151
161
 
152
162
  The workspace-proof axis is cross-cutting. Each proof covers the whole workspace rather than
153
163
  one environment, so each is its own project:
@@ -165,6 +175,7 @@ one environment, so each is its own project:
165
175
  | `distribution` | `tests/distribution.test.ts` | The packed package installs and resolves through its public exports | `prepublishOnly`; absent when private |
166
176
  | `integration` | `tests/integration.test.ts` | The package's features work together end to end across environments | `test` |
167
177
  | `service` | `tests/service/**/*.test.ts` | The live external services this package drives, driven for real | `prepublishOnly`; `test` when private |
178
+ | `studies` | `tests/studies/**/*.test.ts` | The consumer study proofs: installed packages driven through bounded subprocesses and protocol fixtures | `prepublishOnly`; `test` when private |
168
179
 
169
180
  - Define the Node `setup` project when `global` selects its seeded proof or a root file matches
170
181
  `tests/setup*.test.ts`, exact-case, other than `tests/setupBrowser.test.ts` and
@@ -173,6 +184,11 @@ one environment, so each is its own project:
173
184
  exact-case browser proof exists; collect those browser proof paths alone. For each registered project, emit
174
185
  its `test:setup` or `test:setup:browser` script and run it from `test`; otherwise emit neither its
175
186
  project nor its script.
187
+ - Define the `studies` project when at least one exact-case `*.test.ts` file sits beneath
188
+ `tests/studies`, nested or direct, outside any `node_modules` or `.git` directory, which Vitest's
189
+ default exclude never collects. Give it `tests/setup.ts` alone: `tests/setupStudy.ts` is the
190
+ harness module those tests import, and it is no project's setup file. Scaffold plans no
191
+ `tests/setupStudy.ts`, and the root `studies/` directory is no test-project input.
176
192
  - When `tests/setupGlobal.ts` exists, give `src:browser`, `setup:browser`, and `integration` that
177
193
  module as their `globalSetup` option, and give no other project a global setup.
178
194
  - When a browser application selects the journey axis, register `journey:<variant>` projects
@@ -183,15 +199,16 @@ one environment, so each is its own project:
183
199
  from its application project, and run `test:journey` and one `test:journey:<framework>` per
184
200
  app-side browser extension after the application projects in `test`.
185
201
 
186
- `conformance`, `integration`, `distribution`, and `service` are separate subjects, not names for
187
- one.
202
+ `conformance`, `integration`, `distribution`, `studies`, and `service` are separate subjects, not
203
+ names for one.
188
204
  Keep `conformance` in `test`: measure this package against an installed official artifact, and start
189
205
  any server the proof drives itself. Keep `integration` in `test`: compose the workspace's public
190
206
  surfaces without packing, installing, or driving an external service. In a publishing workspace,
191
- run `distribution` and `service` from `prepublishOnly`: pack and install the package in
192
- `distribution`, and drive the real service with `tests/setupService.ts`, longer timeouts, and no file
207
+ run `distribution`, `studies`, and `service` from `prepublishOnly`: pack and install the package in
208
+ `distribution`, run the authored study proofs in `studies` after `npm test` and before the release
209
+ proof, and drive the real service with `tests/setupService.ts`, longer timeouts, and no file
193
210
  parallelism in `service`. In a `private: true` workspace, never declare `prepublishOnly`; omit
194
- `distribution`, reach `service` from `test`, and retain the service project's isolated
211
+ `distribution`, reach `studies` and then `service` from `test`, and retain each isolated project's
195
212
  configuration.
196
213
 
197
214
  One project sits on neither axis. `probe` includes `tmp/probes/**/*.test.ts` so an agent can run a
@@ -221,7 +238,8 @@ Setup assets:
221
238
  Scope with `test:src`, `test:src:core`, `test:src:<face>`, `test:app`, `test:app:server`, and
222
239
  equivalent scripts. Each cross-cutting project has its own script too: `test:policy`,
223
240
  `test:config`, `test:setup`, `test:setup:browser`, `test:journey`, `test:journey:<framework>`,
224
- `test:guides`, `test:conformance`, `test:distribution`, `test:integration`, `test:service`.
241
+ `test:guides`, `test:conformance`, `test:distribution`, `test:integration`, `test:studies`,
242
+ `test:service`.
225
243
 
226
244
  ## Typechecking and environment isolation
227
245
 
@@ -1,4 +1,4 @@
1
- import type { Plugin, Rolldown } from 'vite'
1
+ import type { Plugin, ResolvedConfig, Rolldown } from 'vite'
2
2
  import { parseSync, transformWithOxc, Visitor } from 'vite'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { spawnSync } from 'node:child_process'
@@ -29,6 +29,29 @@ export function hasAsciiUrlControl(value: string): boolean {
29
29
  return false
30
30
  }
31
31
 
32
+ /**
33
+ * Runs a producer once and returns its value, or `undefined` when the producer throws.
34
+ *
35
+ * @remarks
36
+ * This leaf is vendored byte-identical to every workspace, so it imports no result helper and keeps
37
+ * its own value-returning form.
38
+ *
39
+ * @param produce - The producer to run once.
40
+ * @returns The producer's value, or `undefined` when it throws.
41
+ * @example
42
+ * ```ts
43
+ * attemptValue(() => decodeURI('%E0%A4%A')) // undefined
44
+ * attemptValue(() => decodeURI('%20')) // ' '
45
+ * ```
46
+ */
47
+ export function attemptValue<T>(produce: () => T): T | undefined {
48
+ try {
49
+ return produce()
50
+ } catch {
51
+ return undefined
52
+ }
53
+ }
54
+
32
55
  export const PACKAGE_MANIFEST_BYTES = 1_048_576
33
56
  export const ENVIRONMENT_MODULE_BYTES = 8_388_608
34
57
 
@@ -171,18 +194,10 @@ export function isBoundaryExemptModule(id: string): boolean {
171
194
  ) {
172
195
  return true
173
196
  }
174
- let physicalId: string | undefined
175
- try {
176
- physicalId = physicalPath(id).replaceAll('\\', '/')
177
- } catch {
178
- physicalId = undefined
179
- }
180
- for (const candidate of physicalId === undefined ? [path] : [path, physicalId]) {
181
- if (candidate.split('/').some((segment) => segment.toLowerCase() === 'node_modules')) {
182
- return true
183
- }
184
- }
185
- return false
197
+ const physicalId = attemptValue(() => physicalPath(id).replaceAll('\\', '/'))
198
+ return (physicalId === undefined ? [path] : [path, physicalId]).some((candidate) =>
199
+ candidate.split('/').some((segment) => segment.toLowerCase() === 'node_modules'),
200
+ )
186
201
  }
187
202
 
188
203
  export function isWorkspaceBoundaryModule(id: string): boolean {
@@ -190,12 +205,9 @@ export function isWorkspaceBoundaryModule(id: string): boolean {
190
205
  const normalizedId = id.replaceAll('\\', '/')
191
206
  const [path] = normalizedId.split(/[?#]/)
192
207
  if (path === undefined) return false
193
- let candidate = fileSystemPath(path)
194
- try {
195
- if (/^file:/i.test(candidate)) candidate = fileURLToPath(candidate)
196
- } catch {
197
- return false
198
- }
208
+ const system = fileSystemPath(path)
209
+ const candidate = /^file:/i.test(system) ? attemptValue(() => fileURLToPath(system)) : system
210
+ if (candidate === undefined) return false
199
211
  const rootRelative = /^\/(?:app|src)\/(?:core|browser|server|vue)\//.test(candidate)
200
212
  const absoluteCandidate = rootRelative
201
213
  ? resolvePath(WORKSPACE_ROOT, candidate.slice(1))
@@ -247,6 +259,27 @@ export function packageNameOf(source: string): string | undefined {
247
259
  return segments[0]
248
260
  }
249
261
 
262
+ /**
263
+ * Fills a buffer from an open file handle until the handle ends, and reports how many bytes it read.
264
+ *
265
+ * @param handle - The open file descriptor.
266
+ * @param bytes - The buffer to fill; its length is the read ceiling.
267
+ * @param offset - The count of bytes already read into the buffer.
268
+ * @param limit - The largest total count the file may hold.
269
+ * @returns The total count read when the handle ends, or `undefined` when the count passes `limit`.
270
+ */
271
+ export function readHandleBytes(
272
+ handle: number,
273
+ bytes: Buffer,
274
+ offset: number,
275
+ limit: number,
276
+ ): number | undefined {
277
+ const count = readSync(handle, bytes, offset, bytes.length - offset, null)
278
+ if (count === 0) return offset
279
+ const total = offset + count
280
+ return total > limit ? undefined : readHandleBytes(handle, bytes, total, limit)
281
+ }
282
+
250
283
  export function readBoundedFile(path: string, limit: number): string | undefined {
251
284
  if (!existsSync(path)) return undefined
252
285
  try {
@@ -269,13 +302,8 @@ export function readBoundedFile(path: string, limit: number): string | undefined
269
302
  return undefined
270
303
  }
271
304
  const bytes = Buffer.allocUnsafe(current.size + 1)
272
- let offset = 0
273
- for (;;) {
274
- const count = readSync(handle, bytes, offset, bytes.length - offset, null)
275
- if (count === 0) break
276
- offset += count
277
- if (offset > current.size) return undefined
278
- }
305
+ const offset = readHandleBytes(handle, bytes, 0, current.size)
306
+ if (offset === undefined) return undefined
279
307
  const final = fstatSync(handle)
280
308
  if (
281
309
  !final.isFile() ||
@@ -315,10 +343,7 @@ export function packageManifestName(directory: string): string | undefined {
315
343
 
316
344
  export function isPackageBoundary(directory: string): boolean {
317
345
  const segments = directory.replaceAll('\\', '/').split('/')
318
- let nodeModules = -1
319
- for (const [index, segment] of segments.entries()) {
320
- if (segment.toLowerCase() === 'node_modules') nodeModules = index
321
- }
346
+ const nodeModules = segments.findLastIndex((segment) => segment.toLowerCase() === 'node_modules')
322
347
  if (nodeModules < 0) return false
323
348
  const packageSegments = segments.slice(nodeModules + 1)
324
349
  return (
@@ -329,47 +354,39 @@ export function isPackageBoundary(directory: string): boolean {
329
354
  )
330
355
  }
331
356
 
332
- export function packageRootOf(packageName: string, resolvedPath: string): string | undefined {
333
- const physical = physicalPath(resolvedPath)
334
- let current =
335
- existsSync(physical) && lstatSync(physical).isDirectory() ? physical : dirname(physical)
336
- for (;;) {
337
- const boundary = isPackageBoundary(current)
338
- const manifest = resolvePath(current, 'package.json')
339
- if (boundary || existsSync(manifest)) {
340
- return packageManifestName(current) === packageName ? realpathSync.native(current) : undefined
341
- }
342
- const parent = dirname(current)
343
- if (parent === current) return undefined
344
- current = parent
357
+ /**
358
+ * Finds the nearest directory, starting at the given one, that is a package boundary or holds a
359
+ * `package.json`.
360
+ *
361
+ * @param current - The directory to start from.
362
+ * @returns That directory, or `undefined` when none up to the filesystem root qualifies.
363
+ */
364
+ export function findPackageDirectory(current: string): string | undefined {
365
+ if (isPackageBoundary(current) || existsSync(resolvePath(current, 'package.json'))) {
366
+ return current
345
367
  }
368
+ const parent = dirname(current)
369
+ return parent === current ? undefined : findPackageDirectory(parent)
346
370
  }
347
371
 
348
- export function packageRootForResolved(resolvedPath: string): string | undefined {
372
+ export function packageRootOf(packageName: string, resolvedPath: string): string | undefined {
349
373
  const physical = physicalPath(resolvedPath)
350
- let current =
351
- existsSync(physical) && lstatSync(physical).isDirectory() ? physical : dirname(physical)
352
- for (;;) {
353
- const boundary = isPackageBoundary(current)
354
- const manifest = resolvePath(current, 'package.json')
355
- if (boundary || existsSync(manifest)) {
356
- return packageManifestName(current) === undefined ? undefined : realpathSync.native(current)
357
- }
358
- const parent = dirname(current)
359
- if (parent === current) return undefined
360
- current = parent
361
- }
374
+ const directory = findPackageDirectory(
375
+ existsSync(physical) && lstatSync(physical).isDirectory() ? physical : dirname(physical),
376
+ )
377
+ return directory !== undefined && packageManifestName(directory) === packageName
378
+ ? realpathSync.native(directory)
379
+ : undefined
362
380
  }
363
381
 
364
- export function trustedPackageRootFor(
365
- target: string,
366
- trustedPackageRoots: ReadonlySet<string>,
367
- ): string | undefined {
368
- const physical = physicalPath(target)
369
- for (const root of trustedPackageRoots) {
370
- if (containedPath(root, physical)) return root
371
- }
372
- return undefined
382
+ export function packageRootForResolved(resolvedPath: string): string | undefined {
383
+ const physical = physicalPath(resolvedPath)
384
+ const directory = findPackageDirectory(
385
+ existsSync(physical) && lstatSync(physical).isDirectory() ? physical : dirname(physical),
386
+ )
387
+ return directory !== undefined && packageManifestName(directory) !== undefined
388
+ ? realpathSync.native(directory)
389
+ : undefined
373
390
  }
374
391
 
375
392
  export function isStylesheetPath(path: string): boolean {
@@ -487,6 +504,84 @@ export function environmentSourceError(owner: string, source: string): string |
487
504
  return undefined
488
505
  }
489
506
 
507
+ /**
508
+ * Holds what a build plugin reads from the resolved Vite configuration after `configResolved` ran.
509
+ *
510
+ * @remarks
511
+ * Vite runs a plugin hook with its own context as `this`, so a plugin cannot keep this state on
512
+ * itself. Each plugin factory owns one instance and fills it from `configResolved`; before that hook
513
+ * runs, the instance holds the workspace defaults. The environment boundary plugin also keeps the
514
+ * physical package roots it has admitted here, because they outlive each hook call. The admitted
515
+ * roots are read through `locate` and never returned.
516
+ */
517
+ export class BuildState {
518
+ #building = false
519
+ #output: string
520
+ #root = WORKSPACE_ROOT
521
+ #source = ''
522
+ readonly #trusted = new Set<string>()
523
+
524
+ constructor(output = WORKSPACE_ROOT) {
525
+ this.#output = output
526
+ }
527
+
528
+ /** Whether the resolved command is a build. */
529
+ get building(): boolean {
530
+ return this.#building
531
+ }
532
+
533
+ /** The absolute build output directory. */
534
+ get output(): string {
535
+ return this.#output
536
+ }
537
+
538
+ /** The physical project root. */
539
+ get root(): string {
540
+ return this.#root
541
+ }
542
+
543
+ /** The library entry path, or an empty string when the build has none. */
544
+ get source(): string {
545
+ return this.#source
546
+ }
547
+
548
+ /**
549
+ * Admits one physical package root.
550
+ *
551
+ * @param root - The physical package root to admit.
552
+ */
553
+ trust(root: string): void {
554
+ this.#trusted.add(root)
555
+ }
556
+
557
+ /**
558
+ * Finds the admitted package root that contains a path.
559
+ *
560
+ * @param target - The path to place, followed through links to its physical location.
561
+ * @returns The first admitted root, in admission order, that contains the path, or `undefined`
562
+ * when none does.
563
+ */
564
+ locate(target: string): string | undefined {
565
+ const physical = physicalPath(target)
566
+ return [...this.#trusted].find((root) => containedPath(root, physical))
567
+ }
568
+
569
+ /**
570
+ * Reads the command, output directory, project root, and library entry from a resolved config.
571
+ *
572
+ * @param config - The resolved Vite configuration.
573
+ */
574
+ resolve(config: ResolvedConfig): void {
575
+ this.#building = config.command === 'build'
576
+ this.#output = resolvePath(config.root, config.build.outDir)
577
+ this.#root = physicalPath(config.root)
578
+ this.#source =
579
+ config.build.lib !== false && typeof config.build.lib.entry === 'string'
580
+ ? config.build.lib.entry
581
+ : ''
582
+ }
583
+ }
584
+
490
585
  export function enforceOutputPath(configured: string, expected: string): void {
491
586
  if (relative(expected, configured) !== '') {
492
587
  throw new Error(
@@ -501,10 +596,10 @@ export function enforceOutputPath(configured: string, expected: string): void {
501
596
  ) {
502
597
  throw new Error('[orkestrel-output-boundary] Build output must remain inside the workspace')
503
598
  }
504
- let current = WORKSPACE_ROOT
505
- for (const segment of workspaceRelative.split(sep)) {
599
+ const segments = workspaceRelative.split(sep)
600
+ for (const [index, segment] of segments.entries()) {
506
601
  if (segment.length === 0) continue
507
- current = resolvePath(current, segment)
602
+ const current = resolvePath(WORKSPACE_ROOT, ...segments.slice(0, index + 1))
508
603
  if (!existsSync(current)) continue
509
604
  const status = lstatSync(current)
510
605
  if (status.isSymbolicLink() || !status.isDirectory()) {
@@ -520,8 +615,7 @@ export function enforceOutputPath(configured: string, expected: string): void {
520
615
 
521
616
  export function outputBoundary(output: string): Plugin {
522
617
  const expected = resolvePath(WORKSPACE_ROOT, output)
523
- let configured = expected
524
- let build = false
618
+ const state = new BuildState(expected)
525
619
  return {
526
620
  name: 'orkestrel-output-boundary',
527
621
  enforce: 'pre',
@@ -549,11 +643,10 @@ export function outputBoundary(output: string): Plugin {
549
643
  )
550
644
  }
551
645
  }
552
- build = config.command === 'build'
553
- configured = resolvePath(config.root, config.build.outDir)
646
+ state.resolve(config)
554
647
  },
555
648
  buildStart() {
556
- if (build) enforceOutputPath(configured, expected)
649
+ if (state.building) enforceOutputPath(state.output, expected)
557
650
  },
558
651
  }
559
652
  }
@@ -778,23 +871,16 @@ export function rewriteBrowserSpecifier(content: string): string {
778
871
  * or at check.
779
872
  */
780
873
  export function declarationRollup(options: DeclarationRollupOptions): Plugin {
781
- let output = WORKSPACE_ROOT
782
- let source = ''
783
- let build = false
874
+ const state = new BuildState()
784
875
  return {
785
876
  name: 'orkestrel-declaration-rollup',
786
877
  configResolved(config) {
787
- build = config.command === 'build'
788
- output = resolvePath(config.root, config.build.outDir)
789
- source =
790
- config.build.lib !== false && typeof config.build.lib.entry === 'string'
791
- ? config.build.lib.entry
792
- : ''
878
+ state.resolve(config)
793
879
  },
794
880
  closeBundle() {
795
- if (!build) return
796
- const declaration = source.replace(/\.tsx?$/, '.d.ts')
797
- if (declaration === source) {
881
+ if (!state.building) return
882
+ const declaration = state.source.replace(/\.tsx?$/, '.d.ts')
883
+ if (declaration === state.source) {
798
884
  throw new Error(
799
885
  '[orkestrel-declaration-rollup] The face must build one TypeScript library entry',
800
886
  )
@@ -811,7 +897,7 @@ export function declarationRollup(options: DeclarationRollupOptions): Plugin {
811
897
  )
812
898
  }
813
899
  const scratch = mkdtempSync(join(tmpdir(), 'orkestrel-declarations-'))
814
- const rollup = resolvePath(output, 'index.d.ts')
900
+ const rollup = resolvePath(state.output, 'index.d.ts')
815
901
  try {
816
902
  readCompilerOutput(compiler, [
817
903
  '-p',
@@ -906,12 +992,14 @@ export async function environmentAssetSources(
906
992
  const visitor = new Visitor({
907
993
  ImportExpression(node) {
908
994
  if (emitted) return
909
- let value: string | undefined
910
- if (node.source.type === 'Literal' && typeof node.source.value === 'string') {
911
- value = node.source.value
912
- } else if (node.source.type === 'TemplateLiteral' && node.source.expressions.length === 0) {
913
- value = node.source.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join('')
914
- } else {
995
+ const specifier = node.source
996
+ const value =
997
+ specifier.type === 'Literal' && typeof specifier.value === 'string'
998
+ ? specifier.value
999
+ : specifier.type === 'TemplateLiteral' && specifier.expressions.length === 0
1000
+ ? specifier.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join('')
1001
+ : undefined
1002
+ if (value === undefined) {
915
1003
  const location = workspacePath(path)
916
1004
  if (
917
1005
  location !== undefined &&
@@ -945,34 +1033,23 @@ export async function environmentAssetSources(
945
1033
  ) {
946
1034
  return
947
1035
  }
948
- let value: string | undefined
949
- if (source?.type === 'Literal' && typeof source.value === 'string') {
950
- const decoded = decodeAssetSource(source.value)
951
- if (decoded === undefined) {
952
- throw new Error('[orkestrel-environment-boundary] Asset URLs must use valid URI encoding')
953
- }
954
- value = decoded
955
- } else if (source?.type === 'TemplateLiteral') {
956
- const decodedQuasis: string[] = []
957
- for (const quasi of source.quasis) {
958
- const decoded = decodeAssetSource(quasi.value.cooked ?? quasi.value.raw)
959
- if (decoded === undefined) {
960
- throw new Error(
961
- '[orkestrel-environment-boundary] Asset URLs must use valid URI encoding',
962
- )
963
- }
964
- decodedQuasis.push(decoded)
965
- }
966
- if (source.expressions.length > 0) {
967
- throw new Error(
968
- '[orkestrel-environment-boundary] Asset URLs must use static string values',
969
- )
970
- }
971
- value = decodedQuasis.join('__orkestrel__')
972
- } else {
1036
+ const parts =
1037
+ source?.type === 'Literal' && typeof source.value === 'string'
1038
+ ? [source.value]
1039
+ : source?.type === 'TemplateLiteral'
1040
+ ? source.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw)
1041
+ : undefined
1042
+ if (parts === undefined) {
1043
+ throw new Error('[orkestrel-environment-boundary] Asset URLs must use static string values')
1044
+ }
1045
+ const decodedParts = parts.map((part) => decodeAssetSource(part))
1046
+ if (decodedParts.includes(undefined)) {
1047
+ throw new Error('[orkestrel-environment-boundary] Asset URLs must use valid URI encoding')
1048
+ }
1049
+ if (source?.type === 'TemplateLiteral' && source.expressions.length > 0) {
973
1050
  throw new Error('[orkestrel-environment-boundary] Asset URLs must use static string values')
974
1051
  }
975
- if (value === undefined) return
1052
+ const value = decodedParts.join('__orkestrel__')
976
1053
  if (
977
1054
  value.startsWith('.') ||
978
1055
  value.startsWith('/') ||
@@ -998,13 +1075,12 @@ export function environmentBoundary(
998
1075
  | 'app/server'
999
1076
  | 'app/vue',
1000
1077
  ): Plugin {
1001
- const trustedPackageRoots = new Set<string>()
1002
- let environmentRoot = WORKSPACE_ROOT
1078
+ const state = new BuildState()
1003
1079
  return {
1004
1080
  name: 'orkestrel-environment-boundary',
1005
1081
  enforce: 'pre',
1006
1082
  configResolved(config) {
1007
- environmentRoot = physicalPath(config.root)
1083
+ state.resolve(config)
1008
1084
  },
1009
1085
  async resolveId(source, importer) {
1010
1086
  if (importer === undefined || !isWorkspaceBoundaryModule(importer)) return null
@@ -1014,7 +1090,7 @@ export function environmentBoundary(
1014
1090
  if (sourceError !== undefined) this.error(sourceError)
1015
1091
  const importerPath = workspacePath(importer)
1016
1092
  const physicalImporter = physicalPath(importer)
1017
- const importerPackageRoot = trustedPackageRootFor(physicalImporter, trustedPackageRoots)
1093
+ const importerPackageRoot = state.locate(physicalImporter)
1018
1094
  if (importerPath === undefined && importerPackageRoot === undefined) return null
1019
1095
  const [layer, environment] = importerPath?.split('/') ?? []
1020
1096
  if (
@@ -1057,7 +1133,7 @@ export function environmentBoundary(
1057
1133
  'Dependency package imports must resolve inside an exact physical package root',
1058
1134
  )
1059
1135
  }
1060
- trustedPackageRoots.add(mappedPackageRoot)
1136
+ state.trust(mappedPackageRoot)
1061
1137
  } else {
1062
1138
  this.error('Dependency modules cannot import files outside their physical package root')
1063
1139
  }
@@ -1074,7 +1150,7 @@ export function environmentBoundary(
1074
1150
  if (packageRoot === undefined || !containedPath(packageRoot, physicalResolution)) {
1075
1151
  return this.error('Resolved dependencies must remain inside their physical package root')
1076
1152
  }
1077
- trustedPackageRoots.add(packageRoot)
1153
+ state.trust(packageRoot)
1078
1154
  }
1079
1155
  const resolvedSource = workspacePath(resolution?.id ?? fallbackSource)
1080
1156
  if (pathLike && resolvedSource === undefined && importerPackageRoot === undefined) {
@@ -1090,7 +1166,7 @@ export function environmentBoundary(
1090
1166
  async load(id) {
1091
1167
  if (!isWorkspaceBoundaryModule(id)) return null
1092
1168
  const physicalImporter = physicalPath(id)
1093
- const trustedPackageRoot = trustedPackageRootFor(physicalImporter, trustedPackageRoots)
1169
+ const trustedPackageRoot = state.locate(physicalImporter)
1094
1170
  const inferredPackageRoot =
1095
1171
  trustedPackageRoot === undefined ? packageRootForResolved(physicalImporter) : undefined
1096
1172
  const packageRoot =
@@ -1144,12 +1220,12 @@ export function environmentBoundary(
1144
1220
  }
1145
1221
  for (const original of output.originalFileNames) {
1146
1222
  const physical = physicalPath(
1147
- isAbsolute(original) ? original : resolvePath(environmentRoot, original),
1223
+ isAbsolute(original) ? original : resolvePath(state.root, original),
1148
1224
  )
1149
1225
  if (isBoundaryExemptModule(original) || isBoundaryExemptModule(physical)) continue
1150
1226
  const target = workspacePath(physical)
1151
1227
  if (target === undefined) {
1152
- if (trustedPackageRootFor(physical, trustedPackageRoots) === undefined) {
1228
+ if (state.locate(physical) === undefined) {
1153
1229
  this.error('Environment modules cannot import files outside the workspace')
1154
1230
  }
1155
1231
  continue
@@ -1165,10 +1241,7 @@ export function environmentBoundary(
1165
1241
  if (!isWorkspaceBoundaryModule(id)) continue
1166
1242
  const target = workspacePath(id)
1167
1243
  if (target === undefined) {
1168
- if (
1169
- isOutsideWorkspacePath(id) &&
1170
- trustedPackageRootFor(physicalPath(id), trustedPackageRoots) === undefined
1171
- ) {
1244
+ if (isOutsideWorkspacePath(id) && state.locate(physicalPath(id)) === undefined) {
1172
1245
  this.error('Environment modules cannot import files outside the workspace')
1173
1246
  }
1174
1247
  continue
@@ -1183,7 +1256,7 @@ export function environmentBoundary(
1183
1256
  if (!isWorkspaceBoundaryModule(id)) return null
1184
1257
  const target = workspacePath(id)
1185
1258
  const physicalImporter = physicalPath(id)
1186
- const importerPackageRoot = trustedPackageRootFor(physicalImporter, trustedPackageRoots)
1259
+ const importerPackageRoot = state.locate(physicalImporter)
1187
1260
  if (target === undefined) {
1188
1261
  if (isOutsideWorkspacePath(id) && importerPackageRoot === undefined) {
1189
1262
  this.error('Environment modules cannot import files outside the workspace')
@@ -1229,7 +1302,7 @@ export function environmentBoundary(
1229
1302
  'Resolved dependencies must remain inside their physical package root',
1230
1303
  )
1231
1304
  }
1232
- trustedPackageRoots.add(packageRoot)
1305
+ state.trust(packageRoot)
1233
1306
  }
1234
1307
  continue
1235
1308
  }