@orkestrel/scaffold 0.0.101 → 0.0.103
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 +33 -18
- package/dist/host/configs/helpers.ts +209 -148
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/oxlintrc.json +1 -11
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +36 -30
- package/dist/host/tests/config.test.ts +1263 -687
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1699 -1026
- 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,12 +311,9 @@ 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.
|
|
296
|
-
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
`.js`, `.mjs`, `.ts`, and `.mts` modules, and keep ordinary implementation dependencies
|
|
300
|
-
statically named. A source adapter admitted by a runtime loader remains a separately
|
|
301
|
-
selected inventory; it does not widen the loader's built-package admission.
|
|
314
|
+
- Permit computed `import()` in every module, and refuse dynamic `require`. Name ordinary
|
|
315
|
+
implementation dependencies statically. In a module that imports a computed specifier,
|
|
316
|
+
document what selects it in the module's TSDoc.
|
|
302
317
|
- Formatter: Oxfmt with `.oxfmtrc.json`.
|
|
303
318
|
- Bundler: Vite.
|
|
304
319
|
- 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',
|
|
@@ -825,9 +911,8 @@ export function declarationRollup(options: DeclarationRollupOptions): Plugin {
|
|
|
825
911
|
])
|
|
826
912
|
const entry = resolvePath(scratch, relative(scope.root, declaration))
|
|
827
913
|
// A literal `import()` of the extractor reddens `tsc` in a workspace that does not
|
|
828
|
-
// install it,
|
|
829
|
-
//
|
|
830
|
-
// workspace.
|
|
914
|
+
// install it, so the literal `createRequire` call is the form that clears the
|
|
915
|
+
// typecheck in an app-only workspace.
|
|
831
916
|
const loaded: unknown = load('@microsoft/api-extractor')
|
|
832
917
|
if (!isExtractorModule(loaded)) {
|
|
833
918
|
throw new Error(
|
|
@@ -906,24 +991,15 @@ export async function environmentAssetSources(
|
|
|
906
991
|
const visitor = new Visitor({
|
|
907
992
|
ImportExpression(node) {
|
|
908
993
|
if (emitted) return
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
/^(?:src\/server\/artifacts\/|tools\/artifacts\/|\.orkestrel\/[^/]+\/instruments\/(?:[^/]+\/)*tools\/artifacts\/).+\.(?:js|mjs|ts|mts)$/.test(
|
|
919
|
-
location,
|
|
920
|
-
)
|
|
921
|
-
)
|
|
922
|
-
return
|
|
923
|
-
throw new Error(
|
|
924
|
-
'[orkestrel-environment-boundary] Dynamic imports must use static string values',
|
|
925
|
-
)
|
|
926
|
-
}
|
|
994
|
+
const specifier = node.source
|
|
995
|
+
const value =
|
|
996
|
+
specifier.type === 'Literal' && typeof specifier.value === 'string'
|
|
997
|
+
? specifier.value
|
|
998
|
+
: specifier.type === 'TemplateLiteral' && specifier.expressions.length === 0
|
|
999
|
+
? specifier.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join('')
|
|
1000
|
+
: undefined
|
|
1001
|
+
// A computed specifier names no module to inspect statically.
|
|
1002
|
+
if (value === undefined) return
|
|
927
1003
|
const decoded = decodeAssetSource(value)
|
|
928
1004
|
if (decoded === undefined) {
|
|
929
1005
|
throw new Error('[orkestrel-environment-boundary] Module URLs must use valid URI encoding')
|
|
@@ -945,34 +1021,23 @@ export async function environmentAssetSources(
|
|
|
945
1021
|
) {
|
|
946
1022
|
return
|
|
947
1023
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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 {
|
|
1024
|
+
const parts =
|
|
1025
|
+
source?.type === 'Literal' && typeof source.value === 'string'
|
|
1026
|
+
? [source.value]
|
|
1027
|
+
: source?.type === 'TemplateLiteral'
|
|
1028
|
+
? source.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw)
|
|
1029
|
+
: undefined
|
|
1030
|
+
if (parts === undefined) {
|
|
973
1031
|
throw new Error('[orkestrel-environment-boundary] Asset URLs must use static string values')
|
|
974
1032
|
}
|
|
975
|
-
|
|
1033
|
+
const decodedParts = parts.map((part) => decodeAssetSource(part))
|
|
1034
|
+
if (decodedParts.includes(undefined)) {
|
|
1035
|
+
throw new Error('[orkestrel-environment-boundary] Asset URLs must use valid URI encoding')
|
|
1036
|
+
}
|
|
1037
|
+
if (source?.type === 'TemplateLiteral' && source.expressions.length > 0) {
|
|
1038
|
+
throw new Error('[orkestrel-environment-boundary] Asset URLs must use static string values')
|
|
1039
|
+
}
|
|
1040
|
+
const value = decodedParts.join('__orkestrel__')
|
|
976
1041
|
if (
|
|
977
1042
|
value.startsWith('.') ||
|
|
978
1043
|
value.startsWith('/') ||
|
|
@@ -998,13 +1063,12 @@ export function environmentBoundary(
|
|
|
998
1063
|
| 'app/server'
|
|
999
1064
|
| 'app/vue',
|
|
1000
1065
|
): Plugin {
|
|
1001
|
-
const
|
|
1002
|
-
let environmentRoot = WORKSPACE_ROOT
|
|
1066
|
+
const state = new BuildState()
|
|
1003
1067
|
return {
|
|
1004
1068
|
name: 'orkestrel-environment-boundary',
|
|
1005
1069
|
enforce: 'pre',
|
|
1006
1070
|
configResolved(config) {
|
|
1007
|
-
|
|
1071
|
+
state.resolve(config)
|
|
1008
1072
|
},
|
|
1009
1073
|
async resolveId(source, importer) {
|
|
1010
1074
|
if (importer === undefined || !isWorkspaceBoundaryModule(importer)) return null
|
|
@@ -1014,7 +1078,7 @@ export function environmentBoundary(
|
|
|
1014
1078
|
if (sourceError !== undefined) this.error(sourceError)
|
|
1015
1079
|
const importerPath = workspacePath(importer)
|
|
1016
1080
|
const physicalImporter = physicalPath(importer)
|
|
1017
|
-
const importerPackageRoot =
|
|
1081
|
+
const importerPackageRoot = state.locate(physicalImporter)
|
|
1018
1082
|
if (importerPath === undefined && importerPackageRoot === undefined) return null
|
|
1019
1083
|
const [layer, environment] = importerPath?.split('/') ?? []
|
|
1020
1084
|
if (
|
|
@@ -1057,7 +1121,7 @@ export function environmentBoundary(
|
|
|
1057
1121
|
'Dependency package imports must resolve inside an exact physical package root',
|
|
1058
1122
|
)
|
|
1059
1123
|
}
|
|
1060
|
-
|
|
1124
|
+
state.trust(mappedPackageRoot)
|
|
1061
1125
|
} else {
|
|
1062
1126
|
this.error('Dependency modules cannot import files outside their physical package root')
|
|
1063
1127
|
}
|
|
@@ -1074,7 +1138,7 @@ export function environmentBoundary(
|
|
|
1074
1138
|
if (packageRoot === undefined || !containedPath(packageRoot, physicalResolution)) {
|
|
1075
1139
|
return this.error('Resolved dependencies must remain inside their physical package root')
|
|
1076
1140
|
}
|
|
1077
|
-
|
|
1141
|
+
state.trust(packageRoot)
|
|
1078
1142
|
}
|
|
1079
1143
|
const resolvedSource = workspacePath(resolution?.id ?? fallbackSource)
|
|
1080
1144
|
if (pathLike && resolvedSource === undefined && importerPackageRoot === undefined) {
|
|
@@ -1090,7 +1154,7 @@ export function environmentBoundary(
|
|
|
1090
1154
|
async load(id) {
|
|
1091
1155
|
if (!isWorkspaceBoundaryModule(id)) return null
|
|
1092
1156
|
const physicalImporter = physicalPath(id)
|
|
1093
|
-
const trustedPackageRoot =
|
|
1157
|
+
const trustedPackageRoot = state.locate(physicalImporter)
|
|
1094
1158
|
const inferredPackageRoot =
|
|
1095
1159
|
trustedPackageRoot === undefined ? packageRootForResolved(physicalImporter) : undefined
|
|
1096
1160
|
const packageRoot =
|
|
@@ -1144,12 +1208,12 @@ export function environmentBoundary(
|
|
|
1144
1208
|
}
|
|
1145
1209
|
for (const original of output.originalFileNames) {
|
|
1146
1210
|
const physical = physicalPath(
|
|
1147
|
-
isAbsolute(original) ? original : resolvePath(
|
|
1211
|
+
isAbsolute(original) ? original : resolvePath(state.root, original),
|
|
1148
1212
|
)
|
|
1149
1213
|
if (isBoundaryExemptModule(original) || isBoundaryExemptModule(physical)) continue
|
|
1150
1214
|
const target = workspacePath(physical)
|
|
1151
1215
|
if (target === undefined) {
|
|
1152
|
-
if (
|
|
1216
|
+
if (state.locate(physical) === undefined) {
|
|
1153
1217
|
this.error('Environment modules cannot import files outside the workspace')
|
|
1154
1218
|
}
|
|
1155
1219
|
continue
|
|
@@ -1165,10 +1229,7 @@ export function environmentBoundary(
|
|
|
1165
1229
|
if (!isWorkspaceBoundaryModule(id)) continue
|
|
1166
1230
|
const target = workspacePath(id)
|
|
1167
1231
|
if (target === undefined) {
|
|
1168
|
-
if (
|
|
1169
|
-
isOutsideWorkspacePath(id) &&
|
|
1170
|
-
trustedPackageRootFor(physicalPath(id), trustedPackageRoots) === undefined
|
|
1171
|
-
) {
|
|
1232
|
+
if (isOutsideWorkspacePath(id) && state.locate(physicalPath(id)) === undefined) {
|
|
1172
1233
|
this.error('Environment modules cannot import files outside the workspace')
|
|
1173
1234
|
}
|
|
1174
1235
|
continue
|
|
@@ -1183,7 +1244,7 @@ export function environmentBoundary(
|
|
|
1183
1244
|
if (!isWorkspaceBoundaryModule(id)) return null
|
|
1184
1245
|
const target = workspacePath(id)
|
|
1185
1246
|
const physicalImporter = physicalPath(id)
|
|
1186
|
-
const importerPackageRoot =
|
|
1247
|
+
const importerPackageRoot = state.locate(physicalImporter)
|
|
1187
1248
|
if (target === undefined) {
|
|
1188
1249
|
if (isOutsideWorkspacePath(id) && importerPackageRoot === undefined) {
|
|
1189
1250
|
this.error('Environment modules cannot import files outside the workspace')
|
|
@@ -1229,7 +1290,7 @@ export function environmentBoundary(
|
|
|
1229
1290
|
'Resolved dependencies must remain inside their physical package root',
|
|
1230
1291
|
)
|
|
1231
1292
|
}
|
|
1232
|
-
|
|
1293
|
+
state.trust(packageRoot)
|
|
1233
1294
|
}
|
|
1234
1295
|
continue
|
|
1235
1296
|
}
|