@orkestrel/scaffold 0.0.100 → 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.
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +36 -12
- package/dist/host/configs/helpers.ts +214 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/gitignore +1 -0
- package/dist/host/dotfiles/oxlintrc.json +10 -0
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +37 -31
- package/dist/host/tests/config.test.ts +1305 -619
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1710 -995
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- 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
|
|
72
|
-
under `configs/`. Each imports nothing from the workspace, which is what
|
|
73
|
-
`configs/types.ts` exists for one to import: each keeps its own types,
|
|
74
|
-
one file, and the centralized-kind placement in
|
|
75
|
-
leaf. Each `configs/src/*.config.ts` wrapper
|
|
76
|
-
leaves; keep shared build and project
|
|
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
|
|
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`,
|
|
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
|
|
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:
|
|
241
|
+
`test:guides`, `test:conformance`, `test:distribution`, `test:integration`, `test:studies`,
|
|
242
|
+
`test:service`.
|
|
225
243
|
|
|
226
244
|
## Typechecking and environment isolation
|
|
227
245
|
|
|
@@ -293,6 +311,12 @@ Build/check config alignment:
|
|
|
293
311
|
|
|
294
312
|
- Typechecker: `vue-tsc` only where Vue SFCs are checked; `tsc` elsewhere.
|
|
295
313
|
- Linter: Oxlint with `.oxlintrc.json`, independent from typechecking.
|
|
314
|
+
- Place caller-selected local build inspection in `tools/artifacts/` or
|
|
315
|
+
`.orkestrel/<package>/instruments/**/tools/artifacts/`, and published runtime artifact
|
|
316
|
+
loaders in `src/server/artifacts/`. Permit computed ESM imports at those boundaries in
|
|
317
|
+
`.js`, `.mjs`, `.ts`, and `.mts` modules, and keep ordinary implementation dependencies
|
|
318
|
+
statically named. A source adapter admitted by a runtime loader remains a separately
|
|
319
|
+
selected inventory; it does not widen the loader's built-package admission.
|
|
296
320
|
- Formatter: Oxfmt with `.oxfmtrc.json`.
|
|
297
321
|
- Bundler: Vite.
|
|
298
322
|
- Tests: Vitest; `@vitest/browser-playwright` for browser projects.
|
|
@@ -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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
372
|
+
export function packageRootOf(packageName: string, resolvedPath: string): string | undefined {
|
|
349
373
|
const physical = physicalPath(resolvedPath)
|
|
350
|
-
|
|
351
|
-
existsSync(physical) && lstatSync(physical).isDirectory() ? physical : dirname(physical)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
505
|
-
for (const segment of
|
|
599
|
+
const segments = workspaceRelative.split(sep)
|
|
600
|
+
for (const [index, segment] of segments.entries()) {
|
|
506
601
|
if (segment.length === 0) continue
|
|
507
|
-
current = resolvePath(
|
|
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
|
-
|
|
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
|
-
|
|
553
|
-
configured = resolvePath(config.root, config.build.outDir)
|
|
646
|
+
state.resolve(config)
|
|
554
647
|
},
|
|
555
648
|
buildStart() {
|
|
556
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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,22 @@ export async function environmentAssetSources(
|
|
|
906
992
|
const visitor = new Visitor({
|
|
907
993
|
ImportExpression(node) {
|
|
908
994
|
if (emitted) return
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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) {
|
|
1003
|
+
const location = workspacePath(path)
|
|
1004
|
+
if (
|
|
1005
|
+
location !== undefined &&
|
|
1006
|
+
/^(?:src\/server\/artifacts\/|tools\/artifacts\/|\.orkestrel\/[^/]+\/instruments\/(?:[^/]+\/)*tools\/artifacts\/).+\.(?:js|mjs|ts|mts)$/.test(
|
|
1007
|
+
location,
|
|
1008
|
+
)
|
|
1009
|
+
)
|
|
1010
|
+
return
|
|
915
1011
|
throw new Error(
|
|
916
1012
|
'[orkestrel-environment-boundary] Dynamic imports must use static string values',
|
|
917
1013
|
)
|
|
@@ -937,34 +1033,23 @@ export async function environmentAssetSources(
|
|
|
937
1033
|
) {
|
|
938
1034
|
return
|
|
939
1035
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
)
|
|
955
|
-
}
|
|
956
|
-
decodedQuasis.push(decoded)
|
|
957
|
-
}
|
|
958
|
-
if (source.expressions.length > 0) {
|
|
959
|
-
throw new Error(
|
|
960
|
-
'[orkestrel-environment-boundary] Asset URLs must use static string values',
|
|
961
|
-
)
|
|
962
|
-
}
|
|
963
|
-
value = decodedQuasis.join('__orkestrel__')
|
|
964
|
-
} 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) {
|
|
965
1050
|
throw new Error('[orkestrel-environment-boundary] Asset URLs must use static string values')
|
|
966
1051
|
}
|
|
967
|
-
|
|
1052
|
+
const value = decodedParts.join('__orkestrel__')
|
|
968
1053
|
if (
|
|
969
1054
|
value.startsWith('.') ||
|
|
970
1055
|
value.startsWith('/') ||
|
|
@@ -990,13 +1075,12 @@ export function environmentBoundary(
|
|
|
990
1075
|
| 'app/server'
|
|
991
1076
|
| 'app/vue',
|
|
992
1077
|
): Plugin {
|
|
993
|
-
const
|
|
994
|
-
let environmentRoot = WORKSPACE_ROOT
|
|
1078
|
+
const state = new BuildState()
|
|
995
1079
|
return {
|
|
996
1080
|
name: 'orkestrel-environment-boundary',
|
|
997
1081
|
enforce: 'pre',
|
|
998
1082
|
configResolved(config) {
|
|
999
|
-
|
|
1083
|
+
state.resolve(config)
|
|
1000
1084
|
},
|
|
1001
1085
|
async resolveId(source, importer) {
|
|
1002
1086
|
if (importer === undefined || !isWorkspaceBoundaryModule(importer)) return null
|
|
@@ -1006,7 +1090,7 @@ export function environmentBoundary(
|
|
|
1006
1090
|
if (sourceError !== undefined) this.error(sourceError)
|
|
1007
1091
|
const importerPath = workspacePath(importer)
|
|
1008
1092
|
const physicalImporter = physicalPath(importer)
|
|
1009
|
-
const importerPackageRoot =
|
|
1093
|
+
const importerPackageRoot = state.locate(physicalImporter)
|
|
1010
1094
|
if (importerPath === undefined && importerPackageRoot === undefined) return null
|
|
1011
1095
|
const [layer, environment] = importerPath?.split('/') ?? []
|
|
1012
1096
|
if (
|
|
@@ -1049,7 +1133,7 @@ export function environmentBoundary(
|
|
|
1049
1133
|
'Dependency package imports must resolve inside an exact physical package root',
|
|
1050
1134
|
)
|
|
1051
1135
|
}
|
|
1052
|
-
|
|
1136
|
+
state.trust(mappedPackageRoot)
|
|
1053
1137
|
} else {
|
|
1054
1138
|
this.error('Dependency modules cannot import files outside their physical package root')
|
|
1055
1139
|
}
|
|
@@ -1066,7 +1150,7 @@ export function environmentBoundary(
|
|
|
1066
1150
|
if (packageRoot === undefined || !containedPath(packageRoot, physicalResolution)) {
|
|
1067
1151
|
return this.error('Resolved dependencies must remain inside their physical package root')
|
|
1068
1152
|
}
|
|
1069
|
-
|
|
1153
|
+
state.trust(packageRoot)
|
|
1070
1154
|
}
|
|
1071
1155
|
const resolvedSource = workspacePath(resolution?.id ?? fallbackSource)
|
|
1072
1156
|
if (pathLike && resolvedSource === undefined && importerPackageRoot === undefined) {
|
|
@@ -1082,7 +1166,7 @@ export function environmentBoundary(
|
|
|
1082
1166
|
async load(id) {
|
|
1083
1167
|
if (!isWorkspaceBoundaryModule(id)) return null
|
|
1084
1168
|
const physicalImporter = physicalPath(id)
|
|
1085
|
-
const trustedPackageRoot =
|
|
1169
|
+
const trustedPackageRoot = state.locate(physicalImporter)
|
|
1086
1170
|
const inferredPackageRoot =
|
|
1087
1171
|
trustedPackageRoot === undefined ? packageRootForResolved(physicalImporter) : undefined
|
|
1088
1172
|
const packageRoot =
|
|
@@ -1136,12 +1220,12 @@ export function environmentBoundary(
|
|
|
1136
1220
|
}
|
|
1137
1221
|
for (const original of output.originalFileNames) {
|
|
1138
1222
|
const physical = physicalPath(
|
|
1139
|
-
isAbsolute(original) ? original : resolvePath(
|
|
1223
|
+
isAbsolute(original) ? original : resolvePath(state.root, original),
|
|
1140
1224
|
)
|
|
1141
1225
|
if (isBoundaryExemptModule(original) || isBoundaryExemptModule(physical)) continue
|
|
1142
1226
|
const target = workspacePath(physical)
|
|
1143
1227
|
if (target === undefined) {
|
|
1144
|
-
if (
|
|
1228
|
+
if (state.locate(physical) === undefined) {
|
|
1145
1229
|
this.error('Environment modules cannot import files outside the workspace')
|
|
1146
1230
|
}
|
|
1147
1231
|
continue
|
|
@@ -1157,10 +1241,7 @@ export function environmentBoundary(
|
|
|
1157
1241
|
if (!isWorkspaceBoundaryModule(id)) continue
|
|
1158
1242
|
const target = workspacePath(id)
|
|
1159
1243
|
if (target === undefined) {
|
|
1160
|
-
if (
|
|
1161
|
-
isOutsideWorkspacePath(id) &&
|
|
1162
|
-
trustedPackageRootFor(physicalPath(id), trustedPackageRoots) === undefined
|
|
1163
|
-
) {
|
|
1244
|
+
if (isOutsideWorkspacePath(id) && state.locate(physicalPath(id)) === undefined) {
|
|
1164
1245
|
this.error('Environment modules cannot import files outside the workspace')
|
|
1165
1246
|
}
|
|
1166
1247
|
continue
|
|
@@ -1175,7 +1256,7 @@ export function environmentBoundary(
|
|
|
1175
1256
|
if (!isWorkspaceBoundaryModule(id)) return null
|
|
1176
1257
|
const target = workspacePath(id)
|
|
1177
1258
|
const physicalImporter = physicalPath(id)
|
|
1178
|
-
const importerPackageRoot =
|
|
1259
|
+
const importerPackageRoot = state.locate(physicalImporter)
|
|
1179
1260
|
if (target === undefined) {
|
|
1180
1261
|
if (isOutsideWorkspacePath(id) && importerPackageRoot === undefined) {
|
|
1181
1262
|
this.error('Environment modules cannot import files outside the workspace')
|
|
@@ -1221,7 +1302,7 @@ export function environmentBoundary(
|
|
|
1221
1302
|
'Resolved dependencies must remain inside their physical package root',
|
|
1222
1303
|
)
|
|
1223
1304
|
}
|
|
1224
|
-
|
|
1305
|
+
state.trust(packageRoot)
|
|
1225
1306
|
}
|
|
1226
1307
|
continue
|
|
1227
1308
|
}
|