@orkestrel/scaffold 0.0.22 → 0.0.24
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/README.md +84 -99
- package/dist/bin/main.js +1094 -0
- package/dist/bin/main.js.map +1 -0
- package/dist/host/CLAUDE.md +3 -1
- package/dist/host/agents/orchestration.md +61 -4
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +7 -5
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +9 -9
- package/dist/host/claude/rules/architecture.md +45 -3
- package/dist/host/claude/rules/quality.md +4 -0
- package/dist/host/claude/rules/tests.md +57 -1
- package/dist/host/claude/rules/workspace.md +50 -17
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/configs/helpers.ts +762 -0
- package/dist/host/dotfiles/oxlintrc.json +2 -1
- package/dist/host/guides/scaffold.md +862 -0
- package/dist/host/manifest.json +40 -33
- package/dist/host/tests/config.test.ts +544 -0
- package/dist/host/tests/policy.test.ts +46 -0
- package/dist/host/tests/setupPolicy.ts +557 -602
- package/dist/src/core/index.cjs +3569 -10510
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +2361 -2789
- package/dist/src/core/index.d.ts +2361 -2789
- package/dist/src/core/index.js +3513 -10374
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +2855 -3765
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1920 -1335
- package/dist/src/server/index.d.ts +1920 -1335
- package/dist/src/server/index.js +2812 -3680
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -23
- package/dist/bin/scaffold.js +0 -1896
- package/dist/bin/scaffold.js.map +0 -1
- package/dist/host/guides/src/scaffold.md +0 -2886
- /package/dist/host/guides/{src/guide.md → guide.md} +0 -0
package/dist/host/manifest.json
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"entries": [
|
|
3
|
+
{
|
|
4
|
+
"storage": "AGENTS.md",
|
|
5
|
+
"destination": "AGENTS.md",
|
|
6
|
+
"executable": false
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"storage": "CLAUDE.md",
|
|
10
|
+
"destination": "CLAUDE.md",
|
|
11
|
+
"executable": false
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"storage": "LICENSE",
|
|
15
|
+
"destination": "LICENSE",
|
|
16
|
+
"executable": false
|
|
17
|
+
},
|
|
3
18
|
{
|
|
4
19
|
"storage": "agents/orchestration.md",
|
|
5
20
|
"destination": ".agents/orchestration.md",
|
|
@@ -380,6 +395,11 @@
|
|
|
380
395
|
"destination": ".codex/config.toml",
|
|
381
396
|
"executable": false
|
|
382
397
|
},
|
|
398
|
+
{
|
|
399
|
+
"storage": "configs/helpers.ts",
|
|
400
|
+
"destination": "configs/helpers.ts",
|
|
401
|
+
"executable": false
|
|
402
|
+
},
|
|
383
403
|
{
|
|
384
404
|
"storage": "cursor/mcp.json",
|
|
385
405
|
"destination": ".cursor/mcp.json",
|
|
@@ -431,49 +451,44 @@
|
|
|
431
451
|
"executable": false
|
|
432
452
|
},
|
|
433
453
|
{
|
|
434
|
-
"storage": "
|
|
435
|
-
"destination": "
|
|
454
|
+
"storage": "guides/guide.md",
|
|
455
|
+
"destination": "guides/guide.md",
|
|
436
456
|
"executable": false
|
|
437
457
|
},
|
|
438
458
|
{
|
|
439
|
-
"storage": "
|
|
440
|
-
"destination": "
|
|
441
|
-
"executable": false
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
"storage": "LICENSE",
|
|
445
|
-
"destination": "LICENSE",
|
|
446
|
-
"executable": false
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
"storage": "guides/src/guide.md",
|
|
450
|
-
"destination": "guides/src/guide.md",
|
|
451
|
-
"executable": false
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
"storage": "guides/src/scaffold.md",
|
|
455
|
-
"destination": "guides/src/scaffold.md",
|
|
459
|
+
"storage": "guides/scaffold.md",
|
|
460
|
+
"destination": "guides/scaffold.md",
|
|
456
461
|
"executable": false
|
|
457
462
|
},
|
|
458
463
|
{
|
|
459
464
|
"storage": "scripts/codex.sh",
|
|
460
465
|
"destination": "scripts/codex.sh",
|
|
461
|
-
"executable":
|
|
466
|
+
"executable": false
|
|
462
467
|
},
|
|
463
468
|
{
|
|
464
469
|
"storage": "scripts/cursor.sh",
|
|
465
470
|
"destination": "scripts/cursor.sh",
|
|
466
|
-
"executable":
|
|
471
|
+
"executable": false
|
|
467
472
|
},
|
|
468
473
|
{
|
|
469
474
|
"storage": "scripts/deps.sh",
|
|
470
475
|
"destination": "scripts/deps.sh",
|
|
471
|
-
"executable":
|
|
476
|
+
"executable": false
|
|
472
477
|
},
|
|
473
478
|
{
|
|
474
479
|
"storage": "scripts/ollama.sh",
|
|
475
480
|
"destination": "scripts/ollama.sh",
|
|
476
|
-
"executable":
|
|
481
|
+
"executable": false
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"storage": "tests/config.test.ts",
|
|
485
|
+
"destination": "tests/config.test.ts",
|
|
486
|
+
"executable": false
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"storage": "tests/policy.test.ts",
|
|
490
|
+
"destination": "tests/policy.test.ts",
|
|
491
|
+
"executable": false
|
|
477
492
|
},
|
|
478
493
|
{
|
|
479
494
|
"storage": "tests/setupPolicy.ts",
|
|
@@ -482,7 +497,6 @@
|
|
|
482
497
|
}
|
|
483
498
|
],
|
|
484
499
|
"roots": [
|
|
485
|
-
".agents",
|
|
486
500
|
".agents/skills",
|
|
487
501
|
".agents/skills/enterprise-bootstrap",
|
|
488
502
|
".agents/skills/enterprise-bootstrap/agents",
|
|
@@ -504,7 +518,6 @@
|
|
|
504
518
|
".agents/skills/orkestrel-polish-surface",
|
|
505
519
|
".agents/skills/orkestrel-polish-surface/agents",
|
|
506
520
|
".agents/skills/orkestrel-polish-surface/references",
|
|
507
|
-
".claude",
|
|
508
521
|
".claude/agents",
|
|
509
522
|
".claude/rules",
|
|
510
523
|
".claude/skills",
|
|
@@ -515,14 +528,8 @@
|
|
|
515
528
|
".claude/skills/orkestrel-falsify",
|
|
516
529
|
".claude/skills/orkestrel-harden-package",
|
|
517
530
|
".claude/skills/orkestrel-polish-surface",
|
|
518
|
-
".codex",
|
|
519
531
|
".codex/agents",
|
|
520
|
-
".cursor"
|
|
521
|
-
".cursor/rules",
|
|
522
|
-
"guides",
|
|
523
|
-
"guides/src",
|
|
524
|
-
"scripts",
|
|
525
|
-
"tests"
|
|
532
|
+
".cursor/rules"
|
|
526
533
|
],
|
|
527
|
-
"digest": "
|
|
534
|
+
"digest": "b852817ada9fa3b30ce2ee47c7bf786e46dfbc1c5531e4d208b0bf8edcc2482c"
|
|
528
535
|
}
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
// P1: Every checked population must exist and be non-empty; absence fails instead of passing vacuously.
|
|
2
|
+
// P2: Required items are checked strictly; extra items are ignored before their shape is read.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
globSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'node:fs'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { dirname, join, resolve } from 'node:path'
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
17
|
+
import { build, loadConfigFromFile } from 'vite'
|
|
18
|
+
import * as configHelpers from '../configs/helpers.js'
|
|
19
|
+
import configuration, { resolveWorkspacePath } from '../vite.config.js'
|
|
20
|
+
import tsconfig from '../tsconfig.json' with { type: 'json' }
|
|
21
|
+
import { describe, expect, it } from 'vitest'
|
|
22
|
+
|
|
23
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
24
|
+
|
|
25
|
+
describe('root configuration', () => {
|
|
26
|
+
it('resolves every declared alias to its real entry', () => {
|
|
27
|
+
const aliases = configuration.resolve?.alias
|
|
28
|
+
if (typeof aliases !== 'object' || aliases === null || Array.isArray(aliases)) {
|
|
29
|
+
throw new Error('The root configuration carries no alias record')
|
|
30
|
+
}
|
|
31
|
+
const required = new Map<string, string>()
|
|
32
|
+
for (const axis of ['src', 'app']) {
|
|
33
|
+
for (const environment of ['core', 'browser', 'server']) {
|
|
34
|
+
const path = `${axis}/${environment}/index.ts`
|
|
35
|
+
if (existsSync(resolve(root, path))) required.set(`@${axis}/${environment}`, path)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (required.size === 0) throw new Error('The workspace selects no alias target')
|
|
39
|
+
const declared = new Map(Object.entries(tsconfig.compilerOptions.paths))
|
|
40
|
+
const absent = new Map<string, readonly string[]>()
|
|
41
|
+
expect(() => {
|
|
42
|
+
for (const key of required.keys()) {
|
|
43
|
+
if (!absent.has(key)) throw new Error('The alias population carries no required entry')
|
|
44
|
+
}
|
|
45
|
+
}).toThrowError('The alias population carries no required entry')
|
|
46
|
+
for (const [key, expected] of required) {
|
|
47
|
+
const values = declared.get(key)
|
|
48
|
+
if (values === undefined) throw new Error(`${key} is not declared`)
|
|
49
|
+
const [path] = values
|
|
50
|
+
if (path === undefined) throw new Error(`${key} carries no target`)
|
|
51
|
+
const target = resolveWorkspacePath(path)
|
|
52
|
+
expect(existsSync(target)).toBe(true)
|
|
53
|
+
expect(target).toBe(resolve(root, expected))
|
|
54
|
+
expect(Object.getOwnPropertyDescriptor(aliases, key)?.value).toBe(target)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('registers every workspace project with its fixed include and setup files', () => {
|
|
59
|
+
const expected = new Map<
|
|
60
|
+
string,
|
|
61
|
+
{ readonly include: string; readonly setup: readonly string[] }
|
|
62
|
+
>()
|
|
63
|
+
if (existsSync(resolve(root, 'src/core'))) {
|
|
64
|
+
expected.set('src:core', {
|
|
65
|
+
include: 'tests/src/core/**/*.test.ts',
|
|
66
|
+
setup: ['./tests/setup.ts'],
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
if (existsSync(resolve(root, 'src/browser'))) {
|
|
70
|
+
expected.set('src:browser', {
|
|
71
|
+
include: 'tests/src/browser/**/*.test.ts',
|
|
72
|
+
setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
if (existsSync(resolve(root, 'src/server'))) {
|
|
76
|
+
expected.set('src:server', {
|
|
77
|
+
include: 'tests/src/server/**/*.test.ts',
|
|
78
|
+
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
if (existsSync(resolve(root, 'src/bin'))) {
|
|
82
|
+
expected.set('src:bin', {
|
|
83
|
+
include: 'tests/src/bin/**/*.test.ts',
|
|
84
|
+
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
if (existsSync(resolve(root, 'app/core'))) {
|
|
88
|
+
expected.set('app:core', {
|
|
89
|
+
include: 'tests/app/core/**/*.test.ts',
|
|
90
|
+
setup: ['./tests/setup.ts'],
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
if (existsSync(resolve(root, 'app/browser'))) {
|
|
94
|
+
expected.set('app:browser', {
|
|
95
|
+
include: 'tests/app/browser/**/*.test.ts',
|
|
96
|
+
setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
if (existsSync(resolve(root, 'app/server'))) {
|
|
100
|
+
expected.set('app:server', {
|
|
101
|
+
include: 'tests/app/server/**/*.test.ts',
|
|
102
|
+
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
for (const label of ['policy', 'config', 'guides', 'integration']) {
|
|
106
|
+
if (!existsSync(resolve(root, `tests/${label}.test.ts`))) continue
|
|
107
|
+
expected.set(label, {
|
|
108
|
+
include: `tests/${label}.test.ts`,
|
|
109
|
+
setup: ['./tests/setup.ts'],
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
expected.set('probe', { include: 'tmp/probe/**/*.test.ts', setup: ['./tests/setup.ts'] })
|
|
113
|
+
|
|
114
|
+
const projects = configuration.test?.projects
|
|
115
|
+
if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
|
|
116
|
+
let extraLoaded = false
|
|
117
|
+
const control = Object.defineProperty(
|
|
118
|
+
() => {
|
|
119
|
+
extraLoaded = true
|
|
120
|
+
return {
|
|
121
|
+
test: {
|
|
122
|
+
name: { label: 'control' },
|
|
123
|
+
include: ['tests/control.test.ts', 'tests/control.integration.test.ts'],
|
|
124
|
+
setupFiles: ['./tests/setup.ts'],
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
'name',
|
|
129
|
+
{ value: 'control' },
|
|
130
|
+
)
|
|
131
|
+
const controlled = projects.concat(control)
|
|
132
|
+
const configured = new Map<
|
|
133
|
+
string,
|
|
134
|
+
{ readonly include: string; readonly setup: readonly string[] }
|
|
135
|
+
>()
|
|
136
|
+
for (const [requiredLabel] of expected) {
|
|
137
|
+
const factoryName = requiredLabel.replace(/:([a-z])/gu, (_match, letter: string) =>
|
|
138
|
+
letter.toUpperCase(),
|
|
139
|
+
)
|
|
140
|
+
const factory = controlled.find(
|
|
141
|
+
(candidate) => typeof candidate === 'function' && candidate.name === factoryName,
|
|
142
|
+
)
|
|
143
|
+
if (typeof factory !== 'function') {
|
|
144
|
+
throw new Error(`${requiredLabel} has no project factory`)
|
|
145
|
+
}
|
|
146
|
+
const project: unknown = Reflect.apply(factory, undefined, [])
|
|
147
|
+
if (typeof project !== 'object' || project === null) {
|
|
148
|
+
throw new Error('A project factory returned no configuration')
|
|
149
|
+
}
|
|
150
|
+
const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
|
|
151
|
+
if (typeof test !== 'object' || test === null) {
|
|
152
|
+
throw new Error('A project configuration carries no test block')
|
|
153
|
+
}
|
|
154
|
+
const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
|
|
155
|
+
const include: unknown = Object.getOwnPropertyDescriptor(test, 'include')?.value
|
|
156
|
+
const exclude: unknown = Object.getOwnPropertyDescriptor(test, 'exclude')?.value
|
|
157
|
+
const setup: unknown = Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value
|
|
158
|
+
const label =
|
|
159
|
+
typeof name === 'object' && name !== null
|
|
160
|
+
? Object.getOwnPropertyDescriptor(name, 'label')?.value
|
|
161
|
+
: undefined
|
|
162
|
+
if (
|
|
163
|
+
typeof label !== 'string' ||
|
|
164
|
+
!Array.isArray(include) ||
|
|
165
|
+
typeof include[0] !== 'string' ||
|
|
166
|
+
!Array.isArray(setup) ||
|
|
167
|
+
!setup.every((path) => typeof path === 'string')
|
|
168
|
+
) {
|
|
169
|
+
throw new Error('A project does not expose one include and its setup files')
|
|
170
|
+
}
|
|
171
|
+
const effective = include.filter(
|
|
172
|
+
(path) => typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
|
|
173
|
+
)
|
|
174
|
+
if (effective.length !== 1 || typeof effective[0] !== 'string') {
|
|
175
|
+
throw new Error(`${label} does not resolve to one effective include`)
|
|
176
|
+
}
|
|
177
|
+
configured.set(label, { include: effective[0], setup: [...new Set(setup)] })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Required projects come from present source and test paths. Each factory is selected by name
|
|
181
|
+
// before its result is read. Extra factories are ignored without validating their result shape.
|
|
182
|
+
expect(extraLoaded).toBe(false)
|
|
183
|
+
for (const [label, project] of expected) expect(configured.get(label)).toStrictEqual(project)
|
|
184
|
+
|
|
185
|
+
const missing = new Map(configured)
|
|
186
|
+
expect(missing.delete('probe')).toBe(true)
|
|
187
|
+
expect(() => {
|
|
188
|
+
for (const [label, project] of expected) expect(missing.get(label)).toStrictEqual(project)
|
|
189
|
+
}).toThrowError(/strictly equal/u)
|
|
190
|
+
|
|
191
|
+
const misconfigured = new Map(configured)
|
|
192
|
+
const probe = misconfigured.get('probe')
|
|
193
|
+
if (probe === undefined) throw new Error('The configured projects carry no probe control')
|
|
194
|
+
misconfigured.set('probe', { ...probe, setup: ['./tests/setupServer.ts'] })
|
|
195
|
+
expect(() => {
|
|
196
|
+
for (const [label, project] of expected)
|
|
197
|
+
expect(misconfigured.get(label)).toStrictEqual(project)
|
|
198
|
+
}).toThrowError(/strictly equal/u)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('requires and validates every selected target wrapper', async () => {
|
|
202
|
+
const required: string[] = []
|
|
203
|
+
for (const axis of ['src', 'app']) {
|
|
204
|
+
for (const environment of ['core', 'browser', 'server']) {
|
|
205
|
+
if (!existsSync(resolve(root, axis, environment))) continue
|
|
206
|
+
required.push(`configs/${axis}/tsconfig.${environment}.json`)
|
|
207
|
+
if (axis === 'src' || environment !== 'core') {
|
|
208
|
+
required.push(`configs/${axis}/vite.${environment}.config.ts`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (existsSync(resolve(root, 'src/bin'))) {
|
|
213
|
+
required.push('configs/src/tsconfig.bin.json', 'configs/src/vite.bin.config.ts')
|
|
214
|
+
}
|
|
215
|
+
if (existsSync(resolve(root, 'configs/app/vite.showcase.config.ts'))) {
|
|
216
|
+
required.push('configs/app/vite.showcase.config.ts')
|
|
217
|
+
}
|
|
218
|
+
if (required[0] === undefined) {
|
|
219
|
+
throw new Error('The workspace selects no configuration target')
|
|
220
|
+
}
|
|
221
|
+
const found = globSync(
|
|
222
|
+
[
|
|
223
|
+
'configs/src/vite.*.config.ts',
|
|
224
|
+
'configs/app/vite.*.config.ts',
|
|
225
|
+
'configs/src/tsconfig.*.json',
|
|
226
|
+
'configs/app/tsconfig.*.json',
|
|
227
|
+
],
|
|
228
|
+
{ cwd: root },
|
|
229
|
+
).map((path) => path.replaceAll('\\', '/'))
|
|
230
|
+
const extra = 'configs/app/vite.core.config.ts'
|
|
231
|
+
const controlled = found.concat(extra)
|
|
232
|
+
|
|
233
|
+
// Required wrappers come from selected src/app targets. Only that set is loaded and validated.
|
|
234
|
+
// Extra wrappers remain in the found population but are ignored before their content is read.
|
|
235
|
+
expect(controlled).toContain(extra)
|
|
236
|
+
expect(required).not.toContain(extra)
|
|
237
|
+
for (const wrapper of required) {
|
|
238
|
+
expect(controlled).toContain(wrapper)
|
|
239
|
+
const viteMatch =
|
|
240
|
+
/^configs\/(src|app)\/vite\.(core|browser|server|bin|showcase)\.config\.ts$/u.exec(wrapper)
|
|
241
|
+
if (viteMatch !== null) {
|
|
242
|
+
const [, axis, environment] = viteMatch
|
|
243
|
+
if (axis === undefined || environment === undefined) {
|
|
244
|
+
throw new Error(`${wrapper} carries no target`)
|
|
245
|
+
}
|
|
246
|
+
const loaded = await loadConfigFromFile(
|
|
247
|
+
{ command: 'build', mode: 'test', isSsrBuild: false, isPreview: false },
|
|
248
|
+
resolve(root, wrapper),
|
|
249
|
+
root,
|
|
250
|
+
'silent',
|
|
251
|
+
)
|
|
252
|
+
if (loaded === null) throw new Error(`${wrapper} did not load`)
|
|
253
|
+
const output = loaded.config.build?.outDir
|
|
254
|
+
if (output === undefined) throw new Error(`${wrapper} carries no output`)
|
|
255
|
+
const expected =
|
|
256
|
+
environment === 'bin'
|
|
257
|
+
? 'dist/bin'
|
|
258
|
+
: environment === 'showcase'
|
|
259
|
+
? 'dist/showcase'
|
|
260
|
+
: `dist/${axis}/${environment}`
|
|
261
|
+
if (resolve(root, output) !== resolve(root, expected)) {
|
|
262
|
+
throw new Error(`${wrapper} resolves to the wrong output`)
|
|
263
|
+
}
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const tsconfigMatch = /^configs\/(src|app)\/tsconfig\.(core|browser|server|bin)\.json$/u.exec(
|
|
268
|
+
wrapper,
|
|
269
|
+
)
|
|
270
|
+
if (tsconfigMatch === null) {
|
|
271
|
+
throw new Error(`${wrapper} is not a required target wrapper`)
|
|
272
|
+
}
|
|
273
|
+
const [, axis, environment] = tsconfigMatch
|
|
274
|
+
if (axis === undefined || environment === undefined) {
|
|
275
|
+
throw new Error(`${wrapper} carries no TypeScript scope`)
|
|
276
|
+
}
|
|
277
|
+
const parsed: unknown = JSON.parse(readFileSync(resolve(root, wrapper), 'utf8'))
|
|
278
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
279
|
+
throw new Error(`${wrapper} is not a TypeScript configuration record`)
|
|
280
|
+
}
|
|
281
|
+
const compilerOptions: unknown = Object.getOwnPropertyDescriptor(
|
|
282
|
+
parsed,
|
|
283
|
+
'compilerOptions',
|
|
284
|
+
)?.value
|
|
285
|
+
if (typeof compilerOptions !== 'object' || compilerOptions === null) {
|
|
286
|
+
throw new Error(`${wrapper} carries no compiler options`)
|
|
287
|
+
}
|
|
288
|
+
const lib: unknown = Object.getOwnPropertyDescriptor(compilerOptions, 'lib')?.value
|
|
289
|
+
const types: unknown = Object.getOwnPropertyDescriptor(compilerOptions, 'types')?.value
|
|
290
|
+
const expectedLib =
|
|
291
|
+
environment === 'core'
|
|
292
|
+
? ['ESNext', 'WebWorker']
|
|
293
|
+
: environment === 'browser'
|
|
294
|
+
? ['ESNext', 'DOM', 'DOM.Iterable']
|
|
295
|
+
: ['ESNext']
|
|
296
|
+
const expectedTypes =
|
|
297
|
+
environment === 'core'
|
|
298
|
+
? []
|
|
299
|
+
: environment === 'browser'
|
|
300
|
+
? axis === 'app'
|
|
301
|
+
? ['vite/client', 'vue']
|
|
302
|
+
: ['vite/client']
|
|
303
|
+
: ['node']
|
|
304
|
+
expect(lib).toStrictEqual(expectedLib)
|
|
305
|
+
expect(types).toStrictEqual(expectedTypes)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const controlRequired = [
|
|
309
|
+
'configs/app/vite.browser.config.ts',
|
|
310
|
+
'configs/app/vite.server.config.ts',
|
|
311
|
+
]
|
|
312
|
+
const controlFound = ['configs/app/vite.server.config.ts']
|
|
313
|
+
expect(() => {
|
|
314
|
+
for (const wrapper of controlRequired) expect(controlFound).toContain(wrapper)
|
|
315
|
+
}).toThrowError(/vite\.browser/u)
|
|
316
|
+
expect(() =>
|
|
317
|
+
expect(resolve(root, 'dist/actual')).toBe(resolve(root, 'dist/control')),
|
|
318
|
+
).toThrowError(/expected/u)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('registers proof scripts in the correct gate', () => {
|
|
322
|
+
const manifest: unknown = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
|
|
323
|
+
if (typeof manifest !== 'object' || manifest === null) {
|
|
324
|
+
throw new Error('The package manifest is not a record')
|
|
325
|
+
}
|
|
326
|
+
const scripts: unknown = Object.getOwnPropertyDescriptor(manifest, 'scripts')?.value
|
|
327
|
+
if (typeof scripts !== 'object' || scripts === null) {
|
|
328
|
+
throw new Error('The package manifest carries no scripts')
|
|
329
|
+
}
|
|
330
|
+
const test = Object.getOwnPropertyDescriptor(scripts, 'test')?.value
|
|
331
|
+
const config = Object.getOwnPropertyDescriptor(scripts, 'test:config')?.value
|
|
332
|
+
const integration = Object.getOwnPropertyDescriptor(scripts, 'test:integration')?.value
|
|
333
|
+
const publish = Object.getOwnPropertyDescriptor(scripts, 'prepublishOnly')?.value
|
|
334
|
+
const hasIntegration = existsSync(resolve(root, 'tests/integration.test.ts'))
|
|
335
|
+
expect(config).toBe(
|
|
336
|
+
'vitest run --config vite.config.ts --no-cache --reporter=dot --project config',
|
|
337
|
+
)
|
|
338
|
+
expect(typeof test === 'string' && test.includes('npm run test:config')).toBe(true)
|
|
339
|
+
expect(integration).toBe(
|
|
340
|
+
hasIntegration
|
|
341
|
+
? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project integration'
|
|
342
|
+
: undefined,
|
|
343
|
+
)
|
|
344
|
+
expect(typeof test === 'string' && test.includes('test:integration')).toBe(false)
|
|
345
|
+
expect(typeof publish === 'string' && publish.includes('npm run test:integration')).toBe(
|
|
346
|
+
hasIntegration,
|
|
347
|
+
)
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
describe('configuration helpers', () => {
|
|
352
|
+
it('exposes every helper this proof requires', () => {
|
|
353
|
+
const required = [
|
|
354
|
+
'ENVIRONMENT_MODULE_BYTES',
|
|
355
|
+
'PACKAGE_MANIFEST_BYTES',
|
|
356
|
+
'WORKSPACE_ROOT',
|
|
357
|
+
'containedPath',
|
|
358
|
+
'decodeAssetSource',
|
|
359
|
+
'enforceOutputPath',
|
|
360
|
+
'environmentAssetSources',
|
|
361
|
+
'environmentBoundary',
|
|
362
|
+
'environmentPathError',
|
|
363
|
+
'environmentSourceError',
|
|
364
|
+
'fileSystemPath',
|
|
365
|
+
'hasAsciiUrlControl',
|
|
366
|
+
'isBoundaryExemptModule',
|
|
367
|
+
'isOutsideWorkspacePath',
|
|
368
|
+
'isPackageBoundary',
|
|
369
|
+
'isStylesheetPath',
|
|
370
|
+
'isWorkspaceBoundaryModule',
|
|
371
|
+
'outputBoundary',
|
|
372
|
+
'packageManifestName',
|
|
373
|
+
'packageNameOf',
|
|
374
|
+
'packageRootForResolved',
|
|
375
|
+
'packageRootOf',
|
|
376
|
+
'physicalPath',
|
|
377
|
+
'readBoundedFile',
|
|
378
|
+
'sourceFallback',
|
|
379
|
+
'trustedPackageRootFor',
|
|
380
|
+
'workspacePath',
|
|
381
|
+
]
|
|
382
|
+
const found = Object.keys(configHelpers)
|
|
383
|
+
for (const name of required) expect(found).toContain(name)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('resolves contained workspace paths and refuses a real outside sibling', () => {
|
|
387
|
+
const outside = mkdtempSync(join(tmpdir(), 'orkestrel-config-outside-'))
|
|
388
|
+
try {
|
|
389
|
+
const importer = resolve(root, 'tests/config.test.ts')
|
|
390
|
+
expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(root))
|
|
391
|
+
expect(configHelpers.fileSystemPath(`/@fs/${root}`)).toBe(root)
|
|
392
|
+
expect(configHelpers.physicalPath(`${root}?control=true`)).toBe(realpathSync.native(root))
|
|
393
|
+
expect(configHelpers.sourceFallback(importer, './setup.ts')).toBe(
|
|
394
|
+
resolve(root, 'tests/setup.ts'),
|
|
395
|
+
)
|
|
396
|
+
expect(configHelpers.sourceFallback(importer, pathToFileURL(importer).href)).toBe(importer)
|
|
397
|
+
expect(configHelpers.workspacePath(importer)).toBe('tests/config.test.ts')
|
|
398
|
+
expect(configHelpers.containedPath(root, importer)).toBe(true)
|
|
399
|
+
expect(configHelpers.containedPath(root, outside)).toBe(false)
|
|
400
|
+
expect(configHelpers.workspacePath(outside)).toBeUndefined()
|
|
401
|
+
expect(configHelpers.isOutsideWorkspacePath(outside)).toBe(true)
|
|
402
|
+
expect(configHelpers.isOutsideWorkspacePath('src/core/index.ts')).toBe(false)
|
|
403
|
+
} finally {
|
|
404
|
+
rmSync(outside, { recursive: true, force: true })
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('reads bounded files and resolves package roots from real manifests', () => {
|
|
409
|
+
const workspace = mkdtempSync(join(tmpdir(), 'orkestrel-config-package-'))
|
|
410
|
+
try {
|
|
411
|
+
const packageRoot = resolve(workspace, 'node_modules/@sample/package')
|
|
412
|
+
const source = resolve(packageRoot, 'src/index.ts')
|
|
413
|
+
mkdirSync(dirname(source), { recursive: true })
|
|
414
|
+
writeFileSync(resolve(packageRoot, 'package.json'), '{"name":"@sample/package"}', 'utf8')
|
|
415
|
+
writeFileSync(source, 'export {}\n', 'utf8')
|
|
416
|
+
const under = resolve(workspace, 'under.txt')
|
|
417
|
+
const at = resolve(workspace, 'at.txt')
|
|
418
|
+
const over = resolve(workspace, 'over.txt')
|
|
419
|
+
writeFileSync(under, 'abc', 'utf8')
|
|
420
|
+
writeFileSync(at, 'abcd', 'utf8')
|
|
421
|
+
writeFileSync(over, 'abcde', 'utf8')
|
|
422
|
+
|
|
423
|
+
expect(configHelpers.PACKAGE_MANIFEST_BYTES).toBe(1_048_576)
|
|
424
|
+
expect(configHelpers.ENVIRONMENT_MODULE_BYTES).toBe(8_388_608)
|
|
425
|
+
expect(configHelpers.readBoundedFile(under, 4)).toBe('abc')
|
|
426
|
+
expect(configHelpers.readBoundedFile(at, 4)).toBe('abcd')
|
|
427
|
+
expect(configHelpers.readBoundedFile(over, 4)).toBeUndefined()
|
|
428
|
+
expect(configHelpers.packageNameOf('@sample/package/subpath')).toBe('@sample/package')
|
|
429
|
+
expect(configHelpers.packageNameOf('vite/client')).toBe('vite')
|
|
430
|
+
expect(configHelpers.packageNameOf('node:fs')).toBeUndefined()
|
|
431
|
+
expect(configHelpers.packageManifestName(packageRoot)).toBe('@sample/package')
|
|
432
|
+
expect(configHelpers.isPackageBoundary(packageRoot)).toBe(true)
|
|
433
|
+
expect(configHelpers.packageRootOf('@sample/package', source)).toBe(
|
|
434
|
+
realpathSync.native(packageRoot),
|
|
435
|
+
)
|
|
436
|
+
expect(configHelpers.packageRootForResolved(source)).toBe(realpathSync.native(packageRoot))
|
|
437
|
+
expect(
|
|
438
|
+
configHelpers.trustedPackageRootFor(source, new Set([realpathSync.native(packageRoot)])),
|
|
439
|
+
).toBe(realpathSync.native(packageRoot))
|
|
440
|
+
} finally {
|
|
441
|
+
rmSync(workspace, { recursive: true, force: true })
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('classifies module boundaries and extracts static asset sources', async () => {
|
|
446
|
+
const workspace = mkdtempSync(join(tmpdir(), 'orkestrel-config-assets-'))
|
|
447
|
+
try {
|
|
448
|
+
const source = resolve(workspace, 'entry.ts')
|
|
449
|
+
const code =
|
|
450
|
+
"const module = import('./module.js')\nconst asset = new URL('./asset%20name.png', import.meta.url)\nvoid module\nvoid asset\n"
|
|
451
|
+
writeFileSync(source, code, 'utf8')
|
|
452
|
+
|
|
453
|
+
expect(configHelpers.hasAsciiUrlControl('clean')).toBe(false)
|
|
454
|
+
expect(configHelpers.hasAsciiUrlControl('line\u0000break')).toBe(true)
|
|
455
|
+
expect(configHelpers.isBoundaryExemptModule('virtual:control')).toBe(true)
|
|
456
|
+
expect(configHelpers.isBoundaryExemptModule(resolve(root, 'src/core/index.ts'))).toBe(false)
|
|
457
|
+
expect(configHelpers.isWorkspaceBoundaryModule(resolve(root, 'src/core/index.ts'))).toBe(true)
|
|
458
|
+
expect(configHelpers.isWorkspaceBoundaryModule(source)).toBe(false)
|
|
459
|
+
expect(configHelpers.isStylesheetPath('src/styles/index.scss?direct')).toBe(true)
|
|
460
|
+
expect(configHelpers.isStylesheetPath('src/core/index.ts')).toBe(false)
|
|
461
|
+
expect(configHelpers.decodeAssetSource('./asset%20name.png')).toBe('./asset name.png')
|
|
462
|
+
expect(configHelpers.decodeAssetSource('%')).toBeUndefined()
|
|
463
|
+
await expect(
|
|
464
|
+
configHelpers.environmentAssetSources(readFileSync(source, 'utf8'), source),
|
|
465
|
+
).resolves.toStrictEqual(['./module.js', './asset name.png'])
|
|
466
|
+
await expect(
|
|
467
|
+
configHelpers.environmentAssetSources(readFileSync(source, 'utf8'), source, true),
|
|
468
|
+
).resolves.toStrictEqual([])
|
|
469
|
+
} finally {
|
|
470
|
+
rmSync(workspace, { recursive: true, force: true })
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('reports environment and output boundary errors with legal controls', () => {
|
|
475
|
+
expect(configHelpers.environmentPathError('src/browser', 'src/server/index.ts')).toBe(
|
|
476
|
+
'Browser modules cannot depend on Node or server-only modules',
|
|
477
|
+
)
|
|
478
|
+
expect(configHelpers.environmentPathError('src/browser', 'src/core/index.ts')).toBeUndefined()
|
|
479
|
+
expect(configHelpers.environmentSourceError('src/browser', '@src/server')).toBe(
|
|
480
|
+
'Browser modules cannot depend on Node or server-only modules',
|
|
481
|
+
)
|
|
482
|
+
expect(configHelpers.environmentSourceError('src/browser', '@src/core')).toBeUndefined()
|
|
483
|
+
const expected = resolve(root, 'dist/config-control')
|
|
484
|
+
expect(() => configHelpers.enforceOutputPath(expected, expected)).not.toThrow()
|
|
485
|
+
expect(() =>
|
|
486
|
+
configHelpers.enforceOutputPath(resolve(root, 'dist/outside-control'), expected),
|
|
487
|
+
).toThrowError('Build output must use its exact configured workspace directory')
|
|
488
|
+
const outside = resolve(dirname(root), 'outside-config-control')
|
|
489
|
+
expect(() => configHelpers.enforceOutputPath(outside, outside)).toThrowError(
|
|
490
|
+
'Build output must remain inside the workspace',
|
|
491
|
+
)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
it('drives both plugins through their real Vite hooks', async () => {
|
|
495
|
+
const environments: ReadonlyArray<
|
|
496
|
+
'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server'
|
|
497
|
+
> = ['src/core', 'src/browser', 'src/server', 'app/core', 'app/browser', 'app/server']
|
|
498
|
+
const owner = environments.find((environment) => existsSync(resolve(root, environment)))
|
|
499
|
+
if (owner === undefined) throw new Error('The workspace carries no environment plugin target')
|
|
500
|
+
const workspace = mkdtempSync(join(resolve(root, owner), 'config-build-'))
|
|
501
|
+
try {
|
|
502
|
+
const source = resolve(workspace, 'index.ts')
|
|
503
|
+
writeFileSync(source, 'export const control = true\n', 'utf8')
|
|
504
|
+
await expect(
|
|
505
|
+
build({
|
|
506
|
+
root,
|
|
507
|
+
configFile: false,
|
|
508
|
+
logLevel: 'silent',
|
|
509
|
+
publicDir: false,
|
|
510
|
+
plugins: [
|
|
511
|
+
configHelpers.outputBoundary('dist/config-control'),
|
|
512
|
+
configHelpers.environmentBoundary(owner),
|
|
513
|
+
],
|
|
514
|
+
build: {
|
|
515
|
+
write: false,
|
|
516
|
+
outDir: 'dist/config-control',
|
|
517
|
+
lib: {
|
|
518
|
+
entry: source,
|
|
519
|
+
formats: ['es'],
|
|
520
|
+
fileName: () => 'index.js',
|
|
521
|
+
},
|
|
522
|
+
rolldownOptions: { external: [/^node:/u, /^@orkestrel\//u] },
|
|
523
|
+
},
|
|
524
|
+
}),
|
|
525
|
+
).resolves.toBeDefined()
|
|
526
|
+
|
|
527
|
+
const boundary = configHelpers.environmentBoundary('src/browser')
|
|
528
|
+
const hook = boundary.resolveId
|
|
529
|
+
if (typeof hook !== 'function') {
|
|
530
|
+
throw new Error('The environment boundary has no resolve hook')
|
|
531
|
+
}
|
|
532
|
+
const context = {
|
|
533
|
+
error: expect.unreachable,
|
|
534
|
+
resolve: Promise.resolve.bind(Promise, { id: source }),
|
|
535
|
+
}
|
|
536
|
+
await expect(Reflect.apply(hook, context, ['@src/server', source])).rejects.toThrowError(
|
|
537
|
+
'Browser modules cannot depend on Node or server-only modules',
|
|
538
|
+
)
|
|
539
|
+
await expect(Reflect.apply(hook, context, ['@src/core', source])).resolves.toBeNull()
|
|
540
|
+
} finally {
|
|
541
|
+
rmSync(workspace, { recursive: true, force: true })
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
})
|