@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
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
// P1: Every checked population must exist and be non-empty; absence fails instead of passing vacuously.
|
|
2
2
|
// P2: Required items are checked strictly; extra items are ignored before their shape is read.
|
|
3
3
|
|
|
4
|
-
import type { UserConfig } from 'vite'
|
|
4
|
+
import type { InlineConfig, UserConfig } from 'vite'
|
|
5
|
+
import type {
|
|
6
|
+
PolicyComment,
|
|
7
|
+
PolicyContext,
|
|
8
|
+
PolicyDiagnostic,
|
|
9
|
+
PolicyExpression,
|
|
10
|
+
PolicySourceCode,
|
|
11
|
+
} from '../configs/policy.js'
|
|
12
|
+
import type { PolicyProject } from './setupPolicy.js'
|
|
5
13
|
import { spawnSync } from 'node:child_process'
|
|
6
14
|
import {
|
|
15
|
+
closeSync,
|
|
7
16
|
existsSync,
|
|
8
17
|
globSync,
|
|
9
|
-
lstatSync,
|
|
10
18
|
mkdirSync,
|
|
19
|
+
openSync,
|
|
11
20
|
readdirSync,
|
|
12
21
|
readFileSync,
|
|
13
22
|
realpathSync,
|
|
@@ -16,9 +25,9 @@ import {
|
|
|
16
25
|
} from 'node:fs'
|
|
17
26
|
import { createRequire } from 'node:module'
|
|
18
27
|
import { tmpdir } from 'node:os'
|
|
19
|
-
import { dirname, join, relative, resolve } from 'node:path'
|
|
20
|
-
import {
|
|
21
|
-
import { build, createServer, loadConfigFromFile } from 'vite'
|
|
28
|
+
import { dirname, join, parse, relative, resolve } from 'node:path'
|
|
29
|
+
import { pathToFileURL } from 'node:url'
|
|
30
|
+
import { build, createServer, loadConfigFromFile, parseSync, resolveConfig } from 'vite'
|
|
22
31
|
import { RuleTester } from 'oxlint/plugins-dev'
|
|
23
32
|
import * as configHelpers from '../configs/helpers.js'
|
|
24
33
|
import policyPlugin, {
|
|
@@ -49,7 +58,11 @@ import policyPlugin, {
|
|
|
49
58
|
blankPolicyText,
|
|
50
59
|
commentToPolicyParagraph,
|
|
51
60
|
isPolicyVoiced,
|
|
61
|
+
nodeToPolicyOuter,
|
|
52
62
|
paragraphToPolicyOpener,
|
|
63
|
+
programToPolicyDocs,
|
|
64
|
+
reportDomain,
|
|
65
|
+
statementToPolicyBindings,
|
|
53
66
|
stripPolicyCode,
|
|
54
67
|
textToPolicyHits,
|
|
55
68
|
} from '../configs/policy.js'
|
|
@@ -57,7 +70,9 @@ import configuration, { resolveWorkspacePath } from '../vite.config.js'
|
|
|
57
70
|
import * as rootConfiguration from '../vite.config.js'
|
|
58
71
|
import tsconfig from '../tsconfig.json' with { type: 'json' }
|
|
59
72
|
import {
|
|
73
|
+
ARTIFACT_IMPORT_CASES,
|
|
60
74
|
collectSheets,
|
|
75
|
+
collectStudies,
|
|
61
76
|
collectFrameworks,
|
|
62
77
|
readConfigRecord,
|
|
63
78
|
readConfigScript,
|
|
@@ -67,25 +82,29 @@ import {
|
|
|
67
82
|
SHEET_POLICY_BARREL_PATTERN,
|
|
68
83
|
SHEET_POLICY_ORDER_PATTERN,
|
|
69
84
|
readImportDiagnostics,
|
|
85
|
+
createPolicyLazyCases,
|
|
70
86
|
createPolicyScratch,
|
|
87
|
+
createPolicySparse,
|
|
71
88
|
inspectPolicyConfiguration,
|
|
72
89
|
inspectPolicyWiring,
|
|
73
90
|
normalizePolicyFilename,
|
|
91
|
+
POLICY_EXTRACTOR,
|
|
92
|
+
POLICY_LAZY_SHAPES,
|
|
93
|
+
POLICY_PUBLISHES,
|
|
94
|
+
POLICY_ROOT,
|
|
74
95
|
} from './setupPolicy.js'
|
|
75
96
|
import { describe, expect, it } from 'vitest'
|
|
76
97
|
|
|
77
|
-
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
78
|
-
|
|
79
98
|
describe('selected faces', () => {
|
|
80
99
|
it('collects each selected sheet proof from the effective project root and rejects a wrong root', async () => {
|
|
81
|
-
for (const face of collectSheets(
|
|
82
|
-
const proofs = resolve(
|
|
100
|
+
for (const face of collectSheets(POLICY_ROOT)) {
|
|
101
|
+
const proofs = resolve(POLICY_ROOT, 'tests/src', face)
|
|
83
102
|
if (!existsSync(proofs)) continue
|
|
84
|
-
const wrapper = resolve(
|
|
103
|
+
const wrapper = resolve(POLICY_ROOT, `configs/src/vite.${face}.config.ts`)
|
|
85
104
|
const loaded = await loadConfigFromFile(
|
|
86
105
|
{ command: 'serve', mode: 'test' },
|
|
87
106
|
wrapper,
|
|
88
|
-
|
|
107
|
+
POLICY_ROOT,
|
|
89
108
|
'silent',
|
|
90
109
|
)
|
|
91
110
|
if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
|
|
@@ -94,16 +113,19 @@ describe('selected faces', () => {
|
|
|
94
113
|
if (include === undefined) throw new Error(`Missing sheet include ${face}`)
|
|
95
114
|
// Vitest roots a file project at its wrapper directory unless test.root overrides it.
|
|
96
115
|
const directory = resolve(
|
|
97
|
-
|
|
116
|
+
POLICY_ROOT,
|
|
98
117
|
project.test?.root ?? dirname(wrapper),
|
|
99
118
|
project.test?.dir || '.',
|
|
100
119
|
)
|
|
101
120
|
const collected = globSync(include, { cwd: directory }).map((path) =>
|
|
102
121
|
resolve(directory, path),
|
|
103
122
|
)
|
|
104
|
-
const expected = [
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
const expected = [
|
|
124
|
+
resolve(proofs, 'index.test.ts'),
|
|
125
|
+
...(face === 'styles' && existsSync(resolve(proofs, 'themes/index.test.ts'))
|
|
126
|
+
? [resolve(proofs, 'themes/index.test.ts')]
|
|
127
|
+
: []),
|
|
128
|
+
]
|
|
107
129
|
expect(collected).toEqual(expect.arrayContaining(expected))
|
|
108
130
|
const control = globSync(include, { cwd: dirname(wrapper) }).map((path) =>
|
|
109
131
|
resolve(dirname(wrapper), path),
|
|
@@ -150,14 +172,59 @@ describe('selected faces', () => {
|
|
|
150
172
|
}
|
|
151
173
|
})
|
|
152
174
|
|
|
175
|
+
it('collects study tests by exact case, nested or direct, and nothing a lookalike supplies', () => {
|
|
176
|
+
// Each lookalike gets its own workspace, because a host that folds case writes
|
|
177
|
+
// `tests/Studies/adapter.test.ts` into an existing `tests/studies` directory.
|
|
178
|
+
const lookalikes = [
|
|
179
|
+
['no tests directory', []],
|
|
180
|
+
['a types module alone', ['tests/studies/types.ts']],
|
|
181
|
+
['the harness module alone', ['tests/setupStudy.ts']],
|
|
182
|
+
['a root studies directory alone', ['studies/adapter.test.ts']],
|
|
183
|
+
['a case-folded directory', ['tests/Studies/adapter.test.ts']],
|
|
184
|
+
['a test of another suffix', ['tests/studies/adapter.test.js']],
|
|
185
|
+
['a studies entry that is a file', ['tests/studies']],
|
|
186
|
+
[
|
|
187
|
+
'a test inside a nested node_modules',
|
|
188
|
+
['tests/studies/fixture/node_modules/kit/index.test.ts'],
|
|
189
|
+
],
|
|
190
|
+
['a test inside a .git directory', ['tests/studies/fixture/.git/hooks/index.test.ts']],
|
|
191
|
+
] as const
|
|
192
|
+
for (const [label, paths] of lookalikes) {
|
|
193
|
+
const fixture = createPolicyScratch({ prefix: 'config-studies-lookalike-' })
|
|
194
|
+
try {
|
|
195
|
+
for (const path of paths) fixture.write(path, '')
|
|
196
|
+
expect({ label, studies: collectStudies(fixture.path) }).toEqual({ label, studies: [] })
|
|
197
|
+
} finally {
|
|
198
|
+
fixture.destroy()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const fixture = createPolicyScratch({ prefix: 'config-studies-' })
|
|
202
|
+
try {
|
|
203
|
+
for (const path of [
|
|
204
|
+
'tests/studies/types.ts',
|
|
205
|
+
'tests/studies/memory/deep/adapter.test.ts',
|
|
206
|
+
'tests/studies/session.test.ts',
|
|
207
|
+
'tests/studies/memory/adapter.test.js',
|
|
208
|
+
'src/core/index.ts',
|
|
209
|
+
])
|
|
210
|
+
fixture.write(path, '')
|
|
211
|
+
expect(collectStudies(fixture.path)).toEqual([
|
|
212
|
+
'tests/studies/memory/deep/adapter.test.ts',
|
|
213
|
+
'tests/studies/session.test.ts',
|
|
214
|
+
])
|
|
215
|
+
} finally {
|
|
216
|
+
fixture.destroy()
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
153
220
|
it('loads each selected sheet and framework wrapper and checks its packaging and browser project', async () => {
|
|
154
221
|
const manifest = readConfigRecord(
|
|
155
|
-
JSON.parse(readFileSync(resolve(
|
|
222
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
|
|
156
223
|
)
|
|
157
224
|
const scripts = readConfigRecord(manifest.scripts)
|
|
158
|
-
const sheets = collectSheets(
|
|
159
|
-
const frameworks = collectFrameworks(
|
|
160
|
-
const wrappers = collectFaceWrappers(
|
|
225
|
+
const sheets = collectSheets(POLICY_ROOT)
|
|
226
|
+
const frameworks = collectFrameworks(POLICY_ROOT)
|
|
227
|
+
const wrappers = collectFaceWrappers(POLICY_ROOT)
|
|
161
228
|
const readings: Array<{
|
|
162
229
|
readonly axis: string
|
|
163
230
|
readonly face: string
|
|
@@ -170,8 +237,8 @@ describe('selected faces', () => {
|
|
|
170
237
|
for (const wrapper of wrappers) {
|
|
171
238
|
const loaded = await loadConfigFromFile(
|
|
172
239
|
{ command: 'build', mode: 'test' },
|
|
173
|
-
resolve(
|
|
174
|
-
|
|
240
|
+
resolve(POLICY_ROOT, wrapper),
|
|
241
|
+
POLICY_ROOT,
|
|
175
242
|
'silent',
|
|
176
243
|
)
|
|
177
244
|
if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
|
|
@@ -198,7 +265,7 @@ describe('selected faces', () => {
|
|
|
198
265
|
for (const { face, wrapper, project } of readings.filter((reading) =>
|
|
199
266
|
sheets.includes(reading.face),
|
|
200
267
|
)) {
|
|
201
|
-
expect(readFileSync(resolve(
|
|
268
|
+
expect(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')).toContain('sheetProject(')
|
|
202
269
|
expect(inspectSheetConfiguration(project)).toEqual([])
|
|
203
270
|
const exports = readConfigRecord(manifest.exports)
|
|
204
271
|
expect(exports[`./${face}`]).toBe(`./dist/src/${face}/index.css`)
|
|
@@ -221,23 +288,23 @@ describe('selected faces', () => {
|
|
|
221
288
|
for (const { wrapper } of readings.filter(
|
|
222
289
|
(reading) => reading.axis === 'src' && frameworks.includes(`${reading.axis}/${reading.face}`),
|
|
223
290
|
)) {
|
|
224
|
-
const text = readFileSync(resolve(
|
|
291
|
+
const text = readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')
|
|
225
292
|
expect(text).toContain('rewriteCoreSpecifier')
|
|
226
293
|
expect(text).toContain('rewriteBrowserSpecifier')
|
|
227
294
|
}
|
|
228
|
-
if (!existsSync(resolve(
|
|
295
|
+
if (!existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'))) return
|
|
229
296
|
const loaded = await loadConfigFromFile(
|
|
230
297
|
{ command: 'build', mode: 'test' },
|
|
231
|
-
resolve(
|
|
232
|
-
|
|
298
|
+
resolve(POLICY_ROOT, 'configs/src/vite.themes.config.ts'),
|
|
299
|
+
POLICY_ROOT,
|
|
233
300
|
'silent',
|
|
234
301
|
)
|
|
235
302
|
if (loaded === null) throw new Error('Unloaded themes wrapper')
|
|
236
303
|
expect(loaded.config.build?.outDir).toBe('dist/src/styles/themes')
|
|
237
|
-
const barrel = readFileSync(resolve(
|
|
304
|
+
const barrel = readFileSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'), 'utf8')
|
|
238
305
|
expect(readSheetPrelude(barrel)).toMatch(SHEET_POLICY_BARREL_PATTERN)
|
|
239
306
|
expect(
|
|
240
|
-
readSheetPrelude(readFileSync(resolve(
|
|
307
|
+
readSheetPrelude(readFileSync(resolve(POLICY_ROOT, 'src/styles/_tokens.scss'), 'utf8')),
|
|
241
308
|
).toMatch(SHEET_POLICY_ORDER_PATTERN)
|
|
242
309
|
const target = sheets.includes('styles') ? 'styles' : 'themes'
|
|
243
310
|
const command = sheets.includes('styles')
|
|
@@ -271,25 +338,29 @@ describe('selected faces', () => {
|
|
|
271
338
|
it('resolves showcase and journey modes for every occupied application', async () => {
|
|
272
339
|
const applications = [
|
|
273
340
|
'browser',
|
|
274
|
-
...collectFrameworks(
|
|
341
|
+
...collectFrameworks(POLICY_ROOT)
|
|
275
342
|
.filter((face) => face.startsWith('app/'))
|
|
276
343
|
.map((face) => face.slice(4)),
|
|
277
344
|
]
|
|
278
345
|
const manifest = readConfigRecord(
|
|
279
|
-
JSON.parse(readFileSync(resolve(
|
|
346
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
|
|
280
347
|
)
|
|
281
348
|
const scripts = readConfigRecord(manifest.scripts)
|
|
282
349
|
const factories = readConfigRecord(rootConfiguration)
|
|
283
350
|
const showcases = ['configs/app/vite.showcase.config.ts'].filter(
|
|
284
|
-
(path) =>
|
|
351
|
+
(path) =>
|
|
352
|
+
existsSync(resolve(POLICY_ROOT, path)) || existsSync(resolve(POLICY_ROOT, 'showcase')),
|
|
285
353
|
)
|
|
286
354
|
for (const showcase of showcases) {
|
|
287
|
-
const wrapper = readFileSync(
|
|
355
|
+
const wrapper = readFileSync(
|
|
356
|
+
resolve(POLICY_ROOT, 'configs/app/vite.showcase.config.ts'),
|
|
357
|
+
'utf8',
|
|
358
|
+
)
|
|
288
359
|
expect(wrapper).toContain('defineConfig(({ mode }) => appShowcase(mode))')
|
|
289
360
|
expect(wrapper).not.toContain('dist/showcase')
|
|
290
|
-
expect(
|
|
291
|
-
'
|
|
292
|
-
)
|
|
361
|
+
expect(
|
|
362
|
+
readFileSync(resolve(POLICY_ROOT, '.prettierignore'), 'utf8').split(/\r\n|\n/u),
|
|
363
|
+
).toContain('showcase/')
|
|
293
364
|
for (const application of applications) {
|
|
294
365
|
const script = 'build:showcase' + (application === 'browser' ? '' : ':' + application)
|
|
295
366
|
expect(readConfigScript(scripts, script)).toContain('configs/app/vite.showcase.config.ts')
|
|
@@ -301,27 +372,27 @@ describe('selected faces', () => {
|
|
|
301
372
|
}
|
|
302
373
|
const loaded = await loadConfigFromFile(
|
|
303
374
|
{ command: 'build', mode: application },
|
|
304
|
-
resolve(
|
|
305
|
-
|
|
375
|
+
resolve(POLICY_ROOT, showcase),
|
|
376
|
+
POLICY_ROOT,
|
|
306
377
|
'silent',
|
|
307
378
|
)
|
|
308
379
|
if (loaded === null) throw new Error('Unloaded showcase wrapper')
|
|
309
380
|
const output = loaded.config.build?.outDir
|
|
310
381
|
if (output === undefined) throw new Error('Missing showcase output')
|
|
311
|
-
expect(relative(
|
|
382
|
+
expect(relative(POLICY_ROOT, output)).toBe('showcase')
|
|
312
383
|
expect(() =>
|
|
313
|
-
expect(relative(
|
|
384
|
+
expect(relative(POLICY_ROOT, resolve(POLICY_ROOT, 'dist/showcase'))).toBe('showcase'),
|
|
314
385
|
).toThrow('expected')
|
|
315
386
|
expect(loaded.config.build?.emptyOutDir).toBe(false)
|
|
316
387
|
}
|
|
317
388
|
expect(scripts.show).toBeUndefined()
|
|
318
389
|
}
|
|
319
|
-
if (!existsSync(resolve(
|
|
390
|
+
if (!existsSync(resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'))) return
|
|
320
391
|
for (const application of applications) {
|
|
321
392
|
const loaded = await loadConfigFromFile(
|
|
322
393
|
{ command: 'serve', mode: application },
|
|
323
|
-
resolve(
|
|
324
|
-
|
|
394
|
+
resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'),
|
|
395
|
+
POLICY_ROOT,
|
|
325
396
|
'silent',
|
|
326
397
|
)
|
|
327
398
|
if (
|
|
@@ -330,8 +401,8 @@ describe('selected faces', () => {
|
|
|
330
401
|
loaded.config.test.projects.length === 0
|
|
331
402
|
)
|
|
332
403
|
throw new Error('Missing journey projects')
|
|
333
|
-
|
|
334
|
-
const
|
|
404
|
+
// Each awaited factory appends the variant it provides and the variants it declares.
|
|
405
|
+
const provided: Array<{ readonly variant: string; readonly names: readonly string[] }> = []
|
|
335
406
|
for (const factory of loaded.config.test.projects) {
|
|
336
407
|
if (typeof factory !== 'function') throw new Error('Missing journey factory')
|
|
337
408
|
const project = readConfigRecord(
|
|
@@ -343,12 +414,14 @@ describe('selected faces', () => {
|
|
|
343
414
|
const provide = readConfigRecord(test.provide)
|
|
344
415
|
if (typeof provide.variant !== 'string' || !Array.isArray(provide.variants))
|
|
345
416
|
throw new Error('Missing journey variant values')
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
417
|
+
provided.push({
|
|
418
|
+
variant: provide.variant,
|
|
419
|
+
names: provide.variants.map((value: unknown) => {
|
|
420
|
+
const variant = readConfigRecord(value)
|
|
421
|
+
if (typeof variant.name !== 'string') throw new Error('Unnamed journey variant')
|
|
422
|
+
return variant.name
|
|
423
|
+
}),
|
|
424
|
+
})
|
|
352
425
|
const selected = provide.variants
|
|
353
426
|
.map(readConfigRecord)
|
|
354
427
|
.find((variant) => variant.name === provide.variant)
|
|
@@ -370,7 +443,51 @@ describe('selected faces', () => {
|
|
|
370
443
|
expect.arrayContaining(['@orkestrel/test', '@orkestrel/test/browser']),
|
|
371
444
|
)
|
|
372
445
|
}
|
|
373
|
-
expect(
|
|
446
|
+
expect(new Set(provided.map(({ variant }) => variant))).toEqual(
|
|
447
|
+
new Set(provided.flatMap(({ names }) => names)),
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
// The runner loads the wrapper beside this file's own root configuration, so every journey
|
|
451
|
+
// project's record and the commands module's export are one object graph. A wrapper can
|
|
452
|
+
// add commands of its own; it keeps every command the module registers.
|
|
453
|
+
const registered = (
|
|
454
|
+
await Promise.all(
|
|
455
|
+
Object.values(import.meta.glob<unknown>('../configs/commands.ts')).map((load) => load()),
|
|
456
|
+
)
|
|
457
|
+
).map((module) => readConfigRecord(readConfigRecord(module).commands))
|
|
458
|
+
const wrappers = await Promise.all(
|
|
459
|
+
Object.values(
|
|
460
|
+
import.meta.glob<unknown>('../configs/app/vite.journey.config.ts', { import: 'default' }),
|
|
461
|
+
).map((load) => load()),
|
|
462
|
+
)
|
|
463
|
+
expect(wrappers).toHaveLength(1)
|
|
464
|
+
for (const wrapper of wrappers) {
|
|
465
|
+
if (typeof wrapper !== 'function') throw new Error('Missing journey wrapper factory')
|
|
466
|
+
for (const application of applications) {
|
|
467
|
+
const composed = readConfigRecord(
|
|
468
|
+
Reflect.apply(wrapper, undefined, [{ command: 'serve', mode: application }]),
|
|
469
|
+
)
|
|
470
|
+
const projects = readConfigRecord(composed.test).projects
|
|
471
|
+
if (!Array.isArray(projects) || projects.length === 0)
|
|
472
|
+
throw new Error('Missing journey projects')
|
|
473
|
+
for (const project of projects) {
|
|
474
|
+
if (typeof project !== 'function') throw new Error('Missing journey factory')
|
|
475
|
+
const held = readConfigRecord(
|
|
476
|
+
readConfigRecord(
|
|
477
|
+
readConfigRecord(
|
|
478
|
+
readConfigRecord(
|
|
479
|
+
await Reflect.apply(project, undefined, [
|
|
480
|
+
{ command: 'serve', mode: application },
|
|
481
|
+
]),
|
|
482
|
+
).test,
|
|
483
|
+
).browser,
|
|
484
|
+
).commands,
|
|
485
|
+
)
|
|
486
|
+
for (const commands of registered) {
|
|
487
|
+
for (const [key, command] of Object.entries(commands)) expect(held[key]).toBe(command)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
374
491
|
}
|
|
375
492
|
const factory = factories.appJourney
|
|
376
493
|
if (typeof factory !== 'function') throw new Error('Missing application journey')
|
|
@@ -383,15 +500,56 @@ describe('selected faces', () => {
|
|
|
383
500
|
).toThrow('not declared')
|
|
384
501
|
})
|
|
385
502
|
|
|
386
|
-
it('collects both browser setup proofs and optimizes every selected browser factory', () => {
|
|
503
|
+
it('collects both browser setup proofs and optimizes every selected browser factory', async () => {
|
|
387
504
|
const factories = readConfigRecord(rootConfiguration)
|
|
388
505
|
const manifest = readConfigRecord(
|
|
389
|
-
JSON.parse(readFileSync(resolve(
|
|
506
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
|
|
390
507
|
)
|
|
391
508
|
const dependencies = {
|
|
392
509
|
...readConfigRecord(manifest.dependencies ?? {}),
|
|
393
510
|
...readConfigRecord(manifest.devDependencies ?? {}),
|
|
394
511
|
}
|
|
512
|
+
// The runner loads the module this file's root configuration imports, so each factory's
|
|
513
|
+
// record and the module's export are one object graph.
|
|
514
|
+
const registered = (
|
|
515
|
+
await Promise.all(
|
|
516
|
+
Object.values(import.meta.glob<unknown>('../configs/commands.ts')).map((load) => load()),
|
|
517
|
+
)
|
|
518
|
+
).map((module) => readConfigRecord(readConfigRecord(module).commands))
|
|
519
|
+
// The configuration bundle carries `configs/commands.ts` into every load, builds included,
|
|
520
|
+
// and leaves an alias it cannot resolve as a bare package import. The module therefore
|
|
521
|
+
// imports only a `node:` module or a declared package, and loads workspace source inside a
|
|
522
|
+
// command. The control carries both refused forms.
|
|
523
|
+
const control =
|
|
524
|
+
"import { GREETING } from '@src/core'\nexport * from '../src/server/index.js'\nimport { join } from 'node:path'\nimport type { BrowserConfigOptions } from 'vitest/node'\n"
|
|
525
|
+
for (const [path, text, refused] of [
|
|
526
|
+
['control', control, ['@src/core', '../src/server/index.js']],
|
|
527
|
+
...['configs/commands.ts']
|
|
528
|
+
.filter((module) => existsSync(resolve(POLICY_ROOT, module)))
|
|
529
|
+
.map((module) => [module, readFileSync(resolve(POLICY_ROOT, module), 'utf8'), []] as const),
|
|
530
|
+
] as const) {
|
|
531
|
+
const specifiers = parseSync('commands.ts', text).program.body.flatMap((statement) =>
|
|
532
|
+
statement.type === 'ImportDeclaration' ||
|
|
533
|
+
statement.type === 'ExportAllDeclaration' ||
|
|
534
|
+
(statement.type === 'ExportNamedDeclaration' && statement.source !== null)
|
|
535
|
+
? [statement.source?.value ?? '']
|
|
536
|
+
: [],
|
|
537
|
+
)
|
|
538
|
+
expect({
|
|
539
|
+
path,
|
|
540
|
+
refused: specifiers.filter(
|
|
541
|
+
(specifier) =>
|
|
542
|
+
!specifier.startsWith('node:') &&
|
|
543
|
+
!Object.hasOwn(
|
|
544
|
+
dependencies,
|
|
545
|
+
specifier
|
|
546
|
+
.split('/')
|
|
547
|
+
.slice(0, specifier.startsWith('@') ? 2 : 1)
|
|
548
|
+
.join('/'),
|
|
549
|
+
),
|
|
550
|
+
),
|
|
551
|
+
}).toStrictEqual({ path, refused })
|
|
552
|
+
}
|
|
395
553
|
for (const name of [
|
|
396
554
|
'srcBrowser',
|
|
397
555
|
'appBrowser',
|
|
@@ -422,9 +580,13 @@ describe('selected faces', () => {
|
|
|
422
580
|
...(dependencies['@orkestrel/contract'] === undefined ? [] : ['@orkestrel/contract']),
|
|
423
581
|
]),
|
|
424
582
|
)
|
|
583
|
+
for (const commands of registered) {
|
|
584
|
+
const held = readConfigRecord(readConfigRecord(test.browser).commands)
|
|
585
|
+
for (const [key, command] of Object.entries(commands)) expect(held[key]).toBe(command)
|
|
586
|
+
}
|
|
425
587
|
}
|
|
426
588
|
for (const proof of ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts'].filter((path) =>
|
|
427
|
-
existsSync(resolve(
|
|
589
|
+
existsSync(resolve(POLICY_ROOT, path)),
|
|
428
590
|
)) {
|
|
429
591
|
const factory = factories.setupBrowser
|
|
430
592
|
if (typeof factory !== 'function') throw new Error('Missing browser setup project')
|
|
@@ -443,59 +605,32 @@ describe('selected faces', () => {
|
|
|
443
605
|
})
|
|
444
606
|
})
|
|
445
607
|
|
|
446
|
-
// The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
|
|
447
|
-
// a physical directory rather than from a `src` entry of any shape. A regular file, an empty
|
|
448
|
-
// directory, a directory holding no recognized environment, a symbolic link, and an entry this host
|
|
449
|
-
// refuses to inspect each leave a workspace publishing nothing, and reading the entry alone would
|
|
450
|
-
// put every one of them under the face proof. The inspection is caught rather than excused by
|
|
451
|
-
// option: `throwIfNoEntry` excuses an absent path alone and rethrows a permission refusal, and a
|
|
452
|
-
// throw on this line is a collection error that takes this whole file down in that target rather
|
|
453
|
-
// than failing one case. `isPhysicalDirectory` reads the same two facts off an `lstat` and returns
|
|
454
|
-
// false for every inspection error, and this reads them the same way. This is the derivation
|
|
455
|
-
// `targetToEnvironments` uses, which filters the environment names by a contained physical
|
|
456
|
-
// directory, and it answers the same question `blueprintToScripts`
|
|
457
|
-
// answers from a blueprint's own `src` list. A workspace declaring app environments alone publishes
|
|
458
|
-
// no library, so it vendors no `configs/src/` face wrapper and has no roll-up to measure. A face
|
|
459
|
-
// proof is conditioned on this axis rather than on a face wrapper: a workspace that declares the
|
|
460
|
-
// axis and vendors no wrapper carries the defect such a proof reports, so that workspace must still
|
|
461
|
-
// fail the proof. The manifest cases read the same fact from their own `private` flag and name it
|
|
462
|
-
// `manifestPublishes` for the source that decides it there; this reads the axis on disk, so a
|
|
463
|
-
// workspace whose manifest and shape disagree fails one of the two rather than neither.
|
|
464
|
-
const publishes = ['core', 'browser', 'server'].some((environment) => {
|
|
465
|
-
try {
|
|
466
|
-
const entry = lstatSync(resolve(root, 'src', environment))
|
|
467
|
-
return entry.isDirectory() && !entry.isSymbolicLink()
|
|
468
|
-
} catch {
|
|
469
|
-
return false
|
|
470
|
-
}
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
// A declaration roll-up loads the extractor package, which only a workspace publishing source from
|
|
474
|
-
// `src` installs. The resolution below is the mechanism its proof is conditioned on.
|
|
475
|
-
let extractorPath: string | undefined
|
|
476
|
-
try {
|
|
477
|
-
extractorPath = createRequire(import.meta.url).resolve('@microsoft/api-extractor')
|
|
478
|
-
} catch {
|
|
479
|
-
extractorPath = undefined
|
|
480
|
-
}
|
|
481
|
-
|
|
482
608
|
describe('root configuration', () => {
|
|
483
609
|
it('resolves every declared alias to its real entry', () => {
|
|
484
610
|
const aliases = configuration.resolve?.alias
|
|
485
611
|
if (typeof aliases !== 'object' || aliases === null || Array.isArray(aliases)) {
|
|
486
612
|
throw new Error('The root configuration carries no alias record')
|
|
487
613
|
}
|
|
488
|
-
const required = new Map<string, string>(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
614
|
+
const required = new Map<string, string>([
|
|
615
|
+
...['src', 'app'].flatMap((axis) =>
|
|
616
|
+
['core', 'browser', 'server'].flatMap((environment): Array<[string, string]> => {
|
|
617
|
+
const path = `${axis}/${environment}/index.ts`
|
|
618
|
+
return existsSync(resolve(POLICY_ROOT, path)) ? [[`@${axis}/${environment}`, path]] : []
|
|
619
|
+
}),
|
|
620
|
+
),
|
|
621
|
+
...collectSheets(POLICY_ROOT).map((face): [string, string] => [
|
|
622
|
+
`@src/${face}`,
|
|
623
|
+
`src/${face}/index.ts`,
|
|
624
|
+
]),
|
|
625
|
+
...collectFrameworks(POLICY_ROOT).map((face): [string, string] => [
|
|
626
|
+
`@${face}`,
|
|
627
|
+
`${face}/index.ts`,
|
|
628
|
+
]),
|
|
629
|
+
])
|
|
497
630
|
const declared = new Map(Object.entries(tsconfig.compilerOptions.paths))
|
|
498
|
-
expect(
|
|
631
|
+
expect(
|
|
632
|
+
required.size > 0 || existsSync(resolve(POLICY_ROOT, 'src/styles/themes/sheet.ts')),
|
|
633
|
+
).toBe(true)
|
|
499
634
|
if (required.size === 0) {
|
|
500
635
|
if (declared.size !== 0 || Object.keys(aliases).length !== 0)
|
|
501
636
|
throw new Error('The themes-only workspace declares an unexpected alias')
|
|
@@ -514,138 +649,146 @@ describe('root configuration', () => {
|
|
|
514
649
|
if (path === undefined) throw new Error(`${key} carries no target`)
|
|
515
650
|
const target = resolveWorkspacePath(path)
|
|
516
651
|
expect(existsSync(target)).toBe(true)
|
|
517
|
-
expect(target).toBe(resolve(
|
|
652
|
+
expect(target).toBe(resolve(POLICY_ROOT, expected))
|
|
518
653
|
expect(Object.getOwnPropertyDescriptor(aliases, key)?.value).toBe(target)
|
|
519
654
|
}
|
|
520
655
|
})
|
|
521
656
|
|
|
522
657
|
it('registers every workspace project with its fixed include and setup files', () => {
|
|
523
|
-
const expected = new Map<
|
|
524
|
-
string,
|
|
525
|
-
{
|
|
526
|
-
readonly benchmark?: readonly string[]
|
|
527
|
-
readonly include: string | readonly string[]
|
|
528
|
-
readonly parallel?: boolean
|
|
529
|
-
readonly pool?: string
|
|
530
|
-
readonly setup: readonly string[]
|
|
531
|
-
}
|
|
532
|
-
>()
|
|
533
|
-
if (existsSync(resolve(root, 'src/core'))) {
|
|
534
|
-
expected.set('src:core', {
|
|
535
|
-
include: 'tests/src/core/**/*.test.ts',
|
|
536
|
-
setup: ['./tests/setup.ts'],
|
|
537
|
-
})
|
|
538
|
-
}
|
|
539
|
-
if (existsSync(resolve(root, 'src/browser'))) {
|
|
540
|
-
expected.set('src:browser', {
|
|
541
|
-
include: 'tests/src/browser/**/*.test.ts',
|
|
542
|
-
setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
543
|
-
})
|
|
544
|
-
}
|
|
545
|
-
if (existsSync(resolve(root, 'src/server'))) {
|
|
546
|
-
expected.set('src:server', {
|
|
547
|
-
include: 'tests/src/server/**/*.test.ts',
|
|
548
|
-
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
549
|
-
})
|
|
550
|
-
}
|
|
551
|
-
if (existsSync(resolve(root, 'src/bin'))) {
|
|
552
|
-
expected.set('src:bin', {
|
|
553
|
-
include: 'tests/src/bin/**/*.test.ts',
|
|
554
|
-
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
555
|
-
})
|
|
556
|
-
}
|
|
557
|
-
if (existsSync(resolve(root, 'app/core'))) {
|
|
558
|
-
expected.set('app:core', {
|
|
559
|
-
include: 'tests/app/core/**/*.test.ts',
|
|
560
|
-
setup: ['./tests/setup.ts'],
|
|
561
|
-
})
|
|
562
|
-
}
|
|
563
|
-
if (existsSync(resolve(root, 'app/browser'))) {
|
|
564
|
-
expected.set('app:browser', {
|
|
565
|
-
include: 'tests/app/browser/**/*.test.ts',
|
|
566
|
-
setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
567
|
-
})
|
|
568
|
-
}
|
|
569
|
-
if (existsSync(resolve(root, 'app/server'))) {
|
|
570
|
-
expected.set('app:server', {
|
|
571
|
-
include: 'tests/app/server/**/*.test.ts',
|
|
572
|
-
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
573
|
-
})
|
|
574
|
-
}
|
|
575
|
-
// The `configs/agents/tsconfig.skills.json` wrapper selects the `skills` blueprint fact, and the
|
|
576
|
-
// project it registers runs the mirrored proofs under `tests/agents/`.
|
|
577
|
-
if (existsSync(resolve(root, 'configs/agents/tsconfig.skills.json'))) {
|
|
578
|
-
expected.set('skills', {
|
|
579
|
-
include: 'tests/agents/**/*.test.ts',
|
|
580
|
-
setup: ['./tests/setup.ts'],
|
|
581
|
-
})
|
|
582
|
-
}
|
|
583
|
-
for (const label of [
|
|
584
|
-
'policy',
|
|
585
|
-
'config',
|
|
586
|
-
'guides',
|
|
587
|
-
'conformance',
|
|
588
|
-
'distribution',
|
|
589
|
-
'integration',
|
|
590
|
-
]) {
|
|
591
|
-
if (!existsSync(resolve(root, `tests/${label}.test.ts`))) continue
|
|
592
|
-
const setup = ['./tests/setup.ts']
|
|
593
|
-
if (label === 'conformance') setup.push('./tests/setupServer.ts')
|
|
594
|
-
if (
|
|
595
|
-
label === 'integration' &&
|
|
596
|
-
(collectSheets(root).length > 0 ||
|
|
597
|
-
existsSync(resolve(root, 'src/styles/themes/index.scss')))
|
|
598
|
-
)
|
|
599
|
-
setup.push('./tests/setupBrowser.ts', './tests/setupStyles.ts')
|
|
600
|
-
expected.set(label, {
|
|
601
|
-
include: `tests/${label}.test.ts`,
|
|
602
|
-
setup,
|
|
603
|
-
})
|
|
604
|
-
}
|
|
605
658
|
const browserProofs = ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts']
|
|
606
|
-
const setupProofs = globSync('tests/setup*.test.ts', { cwd:
|
|
659
|
+
const setupProofs = globSync('tests/setup*.test.ts', { cwd: POLICY_ROOT })
|
|
607
660
|
.map((path) => path.replaceAll('\\', '/'))
|
|
608
661
|
.filter((path) => /^tests\/setup[^/]*\.test\.ts$/u.test(path))
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
662
|
+
const expected = new Map<string, PolicyProject>([
|
|
663
|
+
// Each row is a project label, the path whose presence selects it, its include, and its setup.
|
|
664
|
+
...(
|
|
665
|
+
[
|
|
666
|
+
['src:core', 'src/core', 'tests/src/core/**/*.test.ts', ['./tests/setup.ts']],
|
|
667
|
+
[
|
|
668
|
+
'src:browser',
|
|
669
|
+
'src/browser',
|
|
670
|
+
'tests/src/browser/**/*.test.ts',
|
|
671
|
+
['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
672
|
+
],
|
|
673
|
+
[
|
|
674
|
+
'src:server',
|
|
675
|
+
'src/server',
|
|
676
|
+
'tests/src/server/**/*.test.ts',
|
|
677
|
+
['./tests/setup.ts', './tests/setupServer.ts'],
|
|
678
|
+
],
|
|
679
|
+
[
|
|
680
|
+
'src:bin',
|
|
681
|
+
'src/bin',
|
|
682
|
+
'tests/src/bin/**/*.test.ts',
|
|
683
|
+
['./tests/setup.ts', './tests/setupServer.ts'],
|
|
684
|
+
],
|
|
685
|
+
['app:core', 'app/core', 'tests/app/core/**/*.test.ts', ['./tests/setup.ts']],
|
|
686
|
+
[
|
|
687
|
+
'app:browser',
|
|
688
|
+
'app/browser',
|
|
689
|
+
'tests/app/browser/**/*.test.ts',
|
|
690
|
+
['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
691
|
+
],
|
|
692
|
+
[
|
|
693
|
+
'app:server',
|
|
694
|
+
'app/server',
|
|
695
|
+
'tests/app/server/**/*.test.ts',
|
|
696
|
+
['./tests/setup.ts', './tests/setupServer.ts'],
|
|
697
|
+
],
|
|
698
|
+
// The `configs/agents/tsconfig.skills.json` wrapper selects the `skills` blueprint fact, and the
|
|
699
|
+
// project it registers runs the mirrored proofs under `tests/agents/`.
|
|
700
|
+
[
|
|
701
|
+
'skills',
|
|
702
|
+
'configs/agents/tsconfig.skills.json',
|
|
703
|
+
'tests/agents/**/*.test.ts',
|
|
704
|
+
['./tests/setup.ts'],
|
|
705
|
+
],
|
|
706
|
+
] as const
|
|
707
|
+
)
|
|
708
|
+
.filter(([, path]) => existsSync(resolve(POLICY_ROOT, path)))
|
|
709
|
+
.map(([label, , include, setup]): [string, PolicyProject] => [label, { include, setup }]),
|
|
710
|
+
...['policy', 'config', 'guides', 'conformance', 'distribution', 'integration']
|
|
711
|
+
.filter((label) => existsSync(resolve(POLICY_ROOT, `tests/${label}.test.ts`)))
|
|
712
|
+
.map((label): [string, PolicyProject] => [
|
|
713
|
+
label,
|
|
714
|
+
{
|
|
715
|
+
include: `tests/${label}.test.ts`,
|
|
716
|
+
setup: [
|
|
717
|
+
'./tests/setup.ts',
|
|
718
|
+
...(label === 'conformance' ? ['./tests/setupServer.ts'] : []),
|
|
719
|
+
...(label === 'integration' &&
|
|
720
|
+
(collectSheets(POLICY_ROOT).length > 0 ||
|
|
721
|
+
existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss')))
|
|
722
|
+
? ['./tests/setupBrowser.ts', './tests/setupStyles.ts']
|
|
723
|
+
: []),
|
|
724
|
+
],
|
|
725
|
+
},
|
|
726
|
+
]),
|
|
727
|
+
...(setupProofs.some((path) => !browserProofs.includes(path))
|
|
728
|
+
? [
|
|
729
|
+
['setup', { include: 'tests/setup*.test.ts', setup: ['./tests/setup.ts'] }] satisfies [
|
|
730
|
+
string,
|
|
731
|
+
PolicyProject,
|
|
732
|
+
],
|
|
733
|
+
]
|
|
734
|
+
: []),
|
|
735
|
+
...(setupProofs.some((path) => browserProofs.includes(path))
|
|
736
|
+
? [
|
|
737
|
+
[
|
|
738
|
+
'setup:browser',
|
|
739
|
+
{ include: browserProofs, setup: ['./tests/setup.ts', './tests/setupBrowser.ts'] },
|
|
740
|
+
] satisfies [string, PolicyProject],
|
|
741
|
+
]
|
|
742
|
+
: []),
|
|
743
|
+
// The live-service project covers a directory rather than one proof, so its
|
|
744
|
+
// readiness module is the fact that selects it. A suite beneath
|
|
745
|
+
// `tests/service` with no setup module is a project nothing configures.
|
|
746
|
+
...(existsSync(resolve(POLICY_ROOT, 'tests/setupService.ts'))
|
|
747
|
+
? [
|
|
748
|
+
[
|
|
749
|
+
'service',
|
|
750
|
+
{
|
|
751
|
+
include: 'tests/service/**/*.test.ts',
|
|
752
|
+
setup: ['./tests/setup.ts', './tests/setupService.ts'],
|
|
753
|
+
},
|
|
754
|
+
] satisfies [string, PolicyProject],
|
|
755
|
+
]
|
|
756
|
+
: []),
|
|
757
|
+
// The study project covers a directory too, and the exact-case test files beneath
|
|
758
|
+
// `tests/studies` select it, outside any `node_modules` or `.git` directory, which Vitest's
|
|
759
|
+
// default exclude never collects. `tests/setupStudy.ts` is a harness module those tests import
|
|
760
|
+
// and no project loads it as a setup file, so the setup list names `tests/setup.ts` alone.
|
|
761
|
+
...(collectStudies(POLICY_ROOT).length > 0
|
|
762
|
+
? [
|
|
763
|
+
[
|
|
764
|
+
'studies',
|
|
765
|
+
{ include: 'tests/studies/**/*.test.ts', setup: ['./tests/setup.ts'] },
|
|
766
|
+
] satisfies [string, PolicyProject],
|
|
767
|
+
]
|
|
768
|
+
: []),
|
|
769
|
+
[
|
|
770
|
+
'probe',
|
|
771
|
+
{
|
|
772
|
+
benchmark: ['tmp/probes/**/*.test.ts', 'tests/**/*.test.ts'],
|
|
773
|
+
include: 'tmp/probes/**/*.test.ts',
|
|
774
|
+
parallel: false,
|
|
775
|
+
pool: 'threads',
|
|
776
|
+
setup: ['./tests/setup.ts'],
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
// A row that is a configuration rather than a factory. Every generated
|
|
780
|
+
// workspace registers the factory itself, so this shape is required by the
|
|
781
|
+
// expectation rather than observed: the proof exercises that resolution wherever it runs
|
|
782
|
+
// instead of only where a hand-written configuration happens to produce it.
|
|
783
|
+
['concrete', { include: 'tests/concrete.test.ts', setup: ['./tests/setup.ts'] }],
|
|
784
|
+
])
|
|
642
785
|
|
|
643
786
|
const projects = configuration.test?.projects
|
|
644
787
|
if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
|
|
645
|
-
|
|
788
|
+
const extraLoads: string[] = []
|
|
646
789
|
const control = Object.defineProperty(
|
|
647
790
|
() => {
|
|
648
|
-
|
|
791
|
+
extraLoads.push('control')
|
|
649
792
|
return {
|
|
650
793
|
test: {
|
|
651
794
|
name: { label: 'control' },
|
|
@@ -665,109 +808,107 @@ describe('root configuration', () => {
|
|
|
665
808
|
},
|
|
666
809
|
}
|
|
667
810
|
const controlled = projects.concat(control, concrete)
|
|
668
|
-
const configured = new Map
|
|
669
|
-
string,
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (typeof candidate !== 'object' || candidate === null) return false
|
|
689
|
-
const block: unknown = Object.getOwnPropertyDescriptor(candidate, 'test')?.value
|
|
690
|
-
if (typeof block !== 'object' || block === null) return false
|
|
691
|
-
const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
|
|
692
|
-
if (typeof named !== 'object' || named === null) return false
|
|
693
|
-
return Object.getOwnPropertyDescriptor(named, 'label')?.value === requiredLabel
|
|
694
|
-
})
|
|
695
|
-
if (row === undefined) {
|
|
696
|
-
throw new Error(`${requiredLabel} has no project factory or configuration`)
|
|
697
|
-
}
|
|
698
|
-
const project: unknown = typeof row === 'function' ? Reflect.apply(row, undefined, []) : row
|
|
699
|
-
if (typeof project !== 'object' || project === null) {
|
|
700
|
-
throw new Error('A project factory returned no configuration')
|
|
701
|
-
}
|
|
702
|
-
const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
|
|
703
|
-
if (typeof test !== 'object' || test === null) {
|
|
704
|
-
throw new Error('A project configuration carries no test block')
|
|
705
|
-
}
|
|
706
|
-
const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
|
|
707
|
-
const include: unknown = Object.getOwnPropertyDescriptor(test, 'include')?.value
|
|
708
|
-
const exclude: unknown = Object.getOwnPropertyDescriptor(test, 'exclude')?.value
|
|
709
|
-
const setup: unknown = Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value
|
|
710
|
-
const label =
|
|
711
|
-
typeof name === 'object' && name !== null
|
|
712
|
-
? Object.getOwnPropertyDescriptor(name, 'label')?.value
|
|
713
|
-
: undefined
|
|
714
|
-
if (
|
|
715
|
-
typeof label !== 'string' ||
|
|
716
|
-
!Array.isArray(include) ||
|
|
717
|
-
typeof include[0] !== 'string' ||
|
|
718
|
-
!Array.isArray(setup) ||
|
|
719
|
-
!setup.every((path) => typeof path === 'string')
|
|
720
|
-
) {
|
|
721
|
-
throw new Error('A project does not expose one include and its setup files')
|
|
722
|
-
}
|
|
723
|
-
const effective = include.filter(
|
|
724
|
-
(path) => typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
|
|
725
|
-
)
|
|
726
|
-
if (
|
|
727
|
-
effective.length !== (label === 'setup:browser' ? 2 : 1) ||
|
|
728
|
-
typeof effective[0] !== 'string'
|
|
729
|
-
) {
|
|
730
|
-
throw new Error(`${label} does not resolve to its required includes`)
|
|
731
|
-
}
|
|
732
|
-
for (const required of label === 'setup' ? browserProofs : [])
|
|
733
|
-
expect(exclude).toContain(required)
|
|
734
|
-
if (label === 'probe') {
|
|
735
|
-
const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
|
|
736
|
-
const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
|
|
737
|
-
const pool: unknown = Object.getOwnPropertyDescriptor(test, 'pool')?.value
|
|
738
|
-
if (typeof benchmark !== 'object' || benchmark === null) {
|
|
739
|
-
throw new Error('The probe project carries no benchmark block')
|
|
811
|
+
const configured = new Map(
|
|
812
|
+
[...expected].map(([requiredLabel]): [string, PolicyProject] => {
|
|
813
|
+
const factoryName = requiredLabel.replace(/:([a-z])/gu, (_match, letter: string) =>
|
|
814
|
+
letter.toUpperCase(),
|
|
815
|
+
)
|
|
816
|
+
// A row is either the factory named for the project or the configuration
|
|
817
|
+
// that project resolves to, and a required project is found as whichever
|
|
818
|
+
// it is. Only the required row is read, so an extra factory is still
|
|
819
|
+
// selected by name and never called.
|
|
820
|
+
const row = controlled.find((candidate) => {
|
|
821
|
+
if (typeof candidate === 'function') return candidate.name === factoryName
|
|
822
|
+
if (typeof candidate !== 'object' || candidate === null) return false
|
|
823
|
+
const block: unknown = Object.getOwnPropertyDescriptor(candidate, 'test')?.value
|
|
824
|
+
if (typeof block !== 'object' || block === null) return false
|
|
825
|
+
const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
|
|
826
|
+
if (typeof named !== 'object' || named === null) return false
|
|
827
|
+
return Object.getOwnPropertyDescriptor(named, 'label')?.value === requiredLabel
|
|
828
|
+
})
|
|
829
|
+
if (row === undefined) {
|
|
830
|
+
throw new Error(`${requiredLabel} has no project factory or configuration`)
|
|
740
831
|
}
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
'
|
|
744
|
-
|
|
832
|
+
const project: unknown = typeof row === 'function' ? Reflect.apply(row, undefined, []) : row
|
|
833
|
+
if (typeof project !== 'object' || project === null) {
|
|
834
|
+
throw new Error('A project factory returned no configuration')
|
|
835
|
+
}
|
|
836
|
+
const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
|
|
837
|
+
if (typeof test !== 'object' || test === null) {
|
|
838
|
+
throw new Error('A project configuration carries no test block')
|
|
839
|
+
}
|
|
840
|
+
const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
|
|
841
|
+
const include: unknown = Object.getOwnPropertyDescriptor(test, 'include')?.value
|
|
842
|
+
const exclude: unknown = Object.getOwnPropertyDescriptor(test, 'exclude')?.value
|
|
843
|
+
const setup: unknown = Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value
|
|
844
|
+
const label =
|
|
845
|
+
typeof name === 'object' && name !== null
|
|
846
|
+
? Object.getOwnPropertyDescriptor(name, 'label')?.value
|
|
847
|
+
: undefined
|
|
745
848
|
if (
|
|
746
|
-
|
|
747
|
-
!
|
|
748
|
-
typeof
|
|
749
|
-
|
|
849
|
+
typeof label !== 'string' ||
|
|
850
|
+
!Array.isArray(include) ||
|
|
851
|
+
typeof include[0] !== 'string' ||
|
|
852
|
+
!Array.isArray(setup) ||
|
|
853
|
+
!setup.every((path) => typeof path === 'string')
|
|
750
854
|
) {
|
|
751
|
-
throw new Error('
|
|
855
|
+
throw new Error('A project does not expose one include and its setup files')
|
|
752
856
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
setup:
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
857
|
+
const effective = include.filter(
|
|
858
|
+
(path) =>
|
|
859
|
+
typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
|
|
860
|
+
)
|
|
861
|
+
if (
|
|
862
|
+
effective.length !== (label === 'setup:browser' ? 2 : 1) ||
|
|
863
|
+
typeof effective[0] !== 'string'
|
|
864
|
+
) {
|
|
865
|
+
throw new Error(`${label} does not resolve to its required includes`)
|
|
866
|
+
}
|
|
867
|
+
for (const required of label === 'setup' ? browserProofs : [])
|
|
868
|
+
expect(exclude).toContain(required)
|
|
869
|
+
if (label === 'probe') {
|
|
870
|
+
const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
|
|
871
|
+
const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
|
|
872
|
+
const pool: unknown = Object.getOwnPropertyDescriptor(test, 'pool')?.value
|
|
873
|
+
if (typeof benchmark !== 'object' || benchmark === null) {
|
|
874
|
+
throw new Error('The probe project carries no benchmark block')
|
|
875
|
+
}
|
|
876
|
+
const benchmarkInclude: unknown = Object.getOwnPropertyDescriptor(
|
|
877
|
+
benchmark,
|
|
878
|
+
'include',
|
|
879
|
+
)?.value
|
|
880
|
+
if (
|
|
881
|
+
!Array.isArray(benchmarkInclude) ||
|
|
882
|
+
!benchmarkInclude.every((path) => typeof path === 'string') ||
|
|
883
|
+
typeof parallel !== 'boolean' ||
|
|
884
|
+
typeof pool !== 'string'
|
|
885
|
+
) {
|
|
886
|
+
throw new Error('The probe project carries an invalid benchmark configuration')
|
|
887
|
+
}
|
|
888
|
+
return [
|
|
889
|
+
label,
|
|
890
|
+
{
|
|
891
|
+
benchmark: benchmarkInclude,
|
|
892
|
+
include: effective[0],
|
|
893
|
+
parallel,
|
|
894
|
+
pool,
|
|
895
|
+
setup: [...new Set(setup)],
|
|
896
|
+
},
|
|
897
|
+
]
|
|
898
|
+
}
|
|
899
|
+
return [
|
|
900
|
+
label,
|
|
901
|
+
{
|
|
902
|
+
include: label === 'setup:browser' ? effective : effective[0],
|
|
903
|
+
setup: [...new Set(setup)],
|
|
904
|
+
},
|
|
905
|
+
]
|
|
906
|
+
}),
|
|
907
|
+
)
|
|
767
908
|
|
|
768
909
|
// Required projects come from present source and test paths. Each factory is selected by name
|
|
769
910
|
// before its result is read. Extra factories are ignored without validating their result shape.
|
|
770
|
-
expect(
|
|
911
|
+
expect(extraLoads).toStrictEqual([])
|
|
771
912
|
for (const [label, project] of expected) expect(configured.get(label)).toStrictEqual(project)
|
|
772
913
|
for (const label of ['setup', 'setup:browser']) {
|
|
773
914
|
const name = label === 'setup' ? 'setup' : 'setupBrowser'
|
|
@@ -823,16 +964,20 @@ describe('root configuration', () => {
|
|
|
823
964
|
value: 'spreading',
|
|
824
965
|
})
|
|
825
966
|
const wrappers = projects.filter((entry) => typeof entry === 'string')
|
|
826
|
-
const sheets = collectSheets(
|
|
827
|
-
const expectedWrappers =
|
|
828
|
-
|
|
829
|
-
|
|
967
|
+
const sheets = collectSheets(POLICY_ROOT)
|
|
968
|
+
const expectedWrappers = [
|
|
969
|
+
...sheets.map((face) => `./configs/src/vite.${face}.config.ts`),
|
|
970
|
+
...(!sheets.includes('styles') &&
|
|
971
|
+
existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'))
|
|
972
|
+
? ['./configs/src/vite.themes.config.ts']
|
|
973
|
+
: []),
|
|
974
|
+
]
|
|
830
975
|
expect([...wrappers].sort()).toEqual(expectedWrappers.sort())
|
|
831
976
|
for (const wrapper of wrappers) {
|
|
832
977
|
const loaded = await loadConfigFromFile(
|
|
833
978
|
{ command: 'serve', mode: invocation.mode },
|
|
834
|
-
resolve(
|
|
835
|
-
|
|
979
|
+
resolve(POLICY_ROOT, wrapper),
|
|
980
|
+
POLICY_ROOT,
|
|
836
981
|
'silent',
|
|
837
982
|
)
|
|
838
983
|
if (loaded === null) throw new Error(`Unloaded file project ${wrapper}`)
|
|
@@ -897,28 +1042,32 @@ describe('root configuration', () => {
|
|
|
897
1042
|
})
|
|
898
1043
|
|
|
899
1044
|
it('requires and validates every selected target wrapper', async () => {
|
|
900
|
-
const required
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1045
|
+
const required = [
|
|
1046
|
+
...['src', 'app'].flatMap((axis) =>
|
|
1047
|
+
['core', 'browser', 'server'].flatMap((environment) =>
|
|
1048
|
+
existsSync(resolve(POLICY_ROOT, axis, environment))
|
|
1049
|
+
? [
|
|
1050
|
+
`configs/${axis}/tsconfig.${environment}.json`,
|
|
1051
|
+
...(axis === 'src' || environment !== 'core'
|
|
1052
|
+
? [`configs/${axis}/vite.${environment}.config.ts`]
|
|
1053
|
+
: []),
|
|
1054
|
+
]
|
|
1055
|
+
: [],
|
|
1056
|
+
),
|
|
1057
|
+
),
|
|
1058
|
+
...collectFrameworks(POLICY_ROOT).map(
|
|
1059
|
+
(face) => `configs/${face.replace('/', '/tsconfig.')}.json`,
|
|
1060
|
+
),
|
|
1061
|
+
...(existsSync(resolve(POLICY_ROOT, 'src/bin'))
|
|
1062
|
+
? ['configs/src/tsconfig.bin.json', 'configs/src/vite.bin.config.ts']
|
|
1063
|
+
: []),
|
|
1064
|
+
...(existsSync(resolve(POLICY_ROOT, 'configs/app/vite.showcase.config.ts'))
|
|
1065
|
+
? ['configs/app/vite.showcase.config.ts']
|
|
1066
|
+
: []),
|
|
1067
|
+
...(existsSync(resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'))
|
|
1068
|
+
? ['configs/app/vite.journey.config.ts']
|
|
1069
|
+
: []),
|
|
1070
|
+
]
|
|
922
1071
|
if (required[0] === undefined) {
|
|
923
1072
|
throw new Error('The workspace selects no configuration target')
|
|
924
1073
|
}
|
|
@@ -929,10 +1078,9 @@ describe('root configuration', () => {
|
|
|
929
1078
|
'configs/src/tsconfig.*.json',
|
|
930
1079
|
'configs/app/tsconfig.*.json',
|
|
931
1080
|
],
|
|
932
|
-
{ cwd:
|
|
1081
|
+
{ cwd: POLICY_ROOT },
|
|
933
1082
|
).map((path) => path.replaceAll('\\', '/'))
|
|
934
1083
|
const extra = 'configs/app/vite.core.config.ts'
|
|
935
|
-
const controlled = found.concat(extra)
|
|
936
1084
|
const planted = createPolicyScratch({ prefix: 'config-journey-' })
|
|
937
1085
|
const journey = 'configs/app/vite.journey.config.ts'
|
|
938
1086
|
planted.write(
|
|
@@ -951,9 +1099,11 @@ describe('root configuration', () => {
|
|
|
951
1099
|
}\n`,
|
|
952
1100
|
)
|
|
953
1101
|
const journeys: object[][] = []
|
|
954
|
-
const targets =
|
|
955
|
-
|
|
956
|
-
|
|
1102
|
+
const targets = [
|
|
1103
|
+
...required.map((wrapper) => ({ wrapper, directory: POLICY_ROOT })),
|
|
1104
|
+
{ wrapper: journey, directory: planted.path },
|
|
1105
|
+
]
|
|
1106
|
+
const controlled = [...found, extra, journey]
|
|
957
1107
|
|
|
958
1108
|
// Required wrappers come from selected src/app targets. Only that set is loaded and validated.
|
|
959
1109
|
// Extra wrappers remain in the found population but are ignored before their content is read.
|
|
@@ -1008,7 +1158,7 @@ describe('root configuration', () => {
|
|
|
1008
1158
|
: environment === 'showcase'
|
|
1009
1159
|
? 'showcase'
|
|
1010
1160
|
: `dist/${axis}/${environment}`
|
|
1011
|
-
if (resolve(
|
|
1161
|
+
if (resolve(POLICY_ROOT, output) !== resolve(POLICY_ROOT, expected)) {
|
|
1012
1162
|
throw new Error(`${wrapper} resolves to the wrong output`)
|
|
1013
1163
|
}
|
|
1014
1164
|
continue
|
|
@@ -1023,7 +1173,7 @@ describe('root configuration', () => {
|
|
|
1023
1173
|
if (axis === undefined || environment === undefined) {
|
|
1024
1174
|
throw new Error(`${wrapper} carries no TypeScript scope`)
|
|
1025
1175
|
}
|
|
1026
|
-
const parsed: unknown = JSON.parse(readFileSync(resolve(
|
|
1176
|
+
const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8'))
|
|
1027
1177
|
if (typeof parsed !== 'object' || parsed === null) {
|
|
1028
1178
|
throw new Error(`${wrapper} is not a TypeScript configuration record`)
|
|
1029
1179
|
}
|
|
@@ -1057,8 +1207,7 @@ describe('root configuration', () => {
|
|
|
1057
1207
|
}
|
|
1058
1208
|
}
|
|
1059
1209
|
for (const tests of journeys) {
|
|
1060
|
-
const names
|
|
1061
|
-
for (const test of tests) {
|
|
1210
|
+
const names = tests.map((test) => {
|
|
1062
1211
|
expect(Object.getOwnPropertyDescriptor(test, 'include')?.value).toStrictEqual([
|
|
1063
1212
|
'tests/app/browser/integration.test.ts',
|
|
1064
1213
|
])
|
|
@@ -1096,9 +1245,9 @@ describe('root configuration', () => {
|
|
|
1096
1245
|
)
|
|
1097
1246
|
)
|
|
1098
1247
|
throw new Error('A journey variant is absent from its declared set')
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1248
|
+
return variant
|
|
1249
|
+
})
|
|
1250
|
+
expect(new Set(names).size).toBe(names.length)
|
|
1102
1251
|
}
|
|
1103
1252
|
} finally {
|
|
1104
1253
|
planted.destroy()
|
|
@@ -1112,13 +1261,13 @@ describe('root configuration', () => {
|
|
|
1112
1261
|
expect(() => {
|
|
1113
1262
|
for (const wrapper of controlRequired) expect(controlFound).toContain(wrapper)
|
|
1114
1263
|
}).toThrow(/vite\.browser/u)
|
|
1115
|
-
expect(() =>
|
|
1116
|
-
/
|
|
1117
|
-
)
|
|
1264
|
+
expect(() =>
|
|
1265
|
+
expect(resolve(POLICY_ROOT, 'dist/actual')).toBe(resolve(POLICY_ROOT, 'dist/control')),
|
|
1266
|
+
).toThrow(/expected/u)
|
|
1118
1267
|
})
|
|
1119
1268
|
|
|
1120
1269
|
it('registers proof scripts in the correct gate', () => {
|
|
1121
|
-
const manifest: unknown = JSON.parse(readFileSync(resolve(
|
|
1270
|
+
const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
|
|
1122
1271
|
if (typeof manifest !== 'object' || manifest === null) {
|
|
1123
1272
|
throw new Error('The package manifest is not a record')
|
|
1124
1273
|
}
|
|
@@ -1133,34 +1282,34 @@ describe('root configuration', () => {
|
|
|
1133
1282
|
const integration = Object.getOwnPropertyDescriptor(scripts, 'test:integration')?.value
|
|
1134
1283
|
const conformance = Object.getOwnPropertyDescriptor(scripts, 'test:conformance')?.value
|
|
1135
1284
|
const service = Object.getOwnPropertyDescriptor(scripts, 'test:service')?.value
|
|
1285
|
+
const studies = Object.getOwnPropertyDescriptor(scripts, 'test:studies')?.value
|
|
1136
1286
|
const publish = Object.getOwnPropertyDescriptor(scripts, 'prepublishOnly')?.value
|
|
1137
|
-
const hasIntegration = existsSync(resolve(
|
|
1287
|
+
const hasIntegration = existsSync(resolve(POLICY_ROOT, 'tests/integration.test.ts'))
|
|
1138
1288
|
// The optional proofs are read off the registered project set rather than off
|
|
1139
1289
|
// their files, because the defect this measures is a registered project no
|
|
1140
1290
|
// gate runs. A project selected by a path that is not yet there is still
|
|
1141
1291
|
// registered, and it is exactly the one whose script goes missing.
|
|
1142
1292
|
const rows = configuration.test?.projects
|
|
1143
1293
|
if (!Array.isArray(rows)) throw new Error('The root configuration carries no projects')
|
|
1144
|
-
const registered = new Set
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
if (typeof label === 'string') registered.add(label)
|
|
1157
|
-
}
|
|
1294
|
+
const registered = new Set(
|
|
1295
|
+
rows.flatMap((row) => {
|
|
1296
|
+
if (typeof row === 'function') return [row.name]
|
|
1297
|
+
if (typeof row !== 'object' || row === null) return []
|
|
1298
|
+
const block: unknown = Object.getOwnPropertyDescriptor(row, 'test')?.value
|
|
1299
|
+
if (typeof block !== 'object' || block === null) return []
|
|
1300
|
+
const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
|
|
1301
|
+
if (typeof named !== 'object' || named === null) return []
|
|
1302
|
+
const label: unknown = Object.getOwnPropertyDescriptor(named, 'label')?.value
|
|
1303
|
+
return typeof label === 'string' ? [label] : []
|
|
1304
|
+
}),
|
|
1305
|
+
)
|
|
1158
1306
|
// The population must be able to answer both ways before either answer counts.
|
|
1159
1307
|
expect(registered.has('config')).toBe(true)
|
|
1160
1308
|
expect(registered.has('control')).toBe(false)
|
|
1161
1309
|
const hasConformance = registered.has('conformance')
|
|
1162
1310
|
const hasDistribution = registered.has('distribution')
|
|
1163
1311
|
const hasService = registered.has('service')
|
|
1312
|
+
const hasStudies = registered.has('studies')
|
|
1164
1313
|
expect(config).toBe(
|
|
1165
1314
|
'vitest run --config vite.config.ts --no-cache --reporter=dot --project config',
|
|
1166
1315
|
)
|
|
@@ -1211,10 +1360,28 @@ describe('root configuration', () => {
|
|
|
1211
1360
|
expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(
|
|
1212
1361
|
hasService && manifestPublishes,
|
|
1213
1362
|
)
|
|
1363
|
+
// The study proof spawns processes, so it follows the same split as the live-service proof:
|
|
1364
|
+
// the publish gate of a publishing workspace runs it after `npm test` and before the release
|
|
1365
|
+
// proof, and a private workspace reaches it from `test`.
|
|
1366
|
+
expect(studies).toBe(
|
|
1367
|
+
hasStudies
|
|
1368
|
+
? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project studies'
|
|
1369
|
+
: undefined,
|
|
1370
|
+
)
|
|
1371
|
+
expect(typeof test === 'string' && test.includes('npm run test:studies')).toBe(
|
|
1372
|
+
hasStudies && !manifestPublishes,
|
|
1373
|
+
)
|
|
1374
|
+
expect(typeof publish === 'string' && publish.includes('npm run test:studies')).toBe(
|
|
1375
|
+
hasStudies && manifestPublishes,
|
|
1376
|
+
)
|
|
1377
|
+
const steps = ['npm test &&', 'npm run test:studies', 'npm run test:distribution']
|
|
1378
|
+
const positions = typeof publish === 'string' ? steps.map((step) => publish.indexOf(step)) : []
|
|
1379
|
+
const sequenced = positions.every((position, index) => position > (positions[index - 1] ?? -1))
|
|
1380
|
+
expect(sequenced || !(hasStudies && hasDistribution && manifestPublishes)).toBe(true)
|
|
1214
1381
|
})
|
|
1215
1382
|
|
|
1216
1383
|
it('rebuilds publishing workspaces before packing', () => {
|
|
1217
|
-
const manifest: unknown = JSON.parse(readFileSync(resolve(
|
|
1384
|
+
const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
|
|
1218
1385
|
if (typeof manifest !== 'object' || manifest === null) {
|
|
1219
1386
|
throw new Error('The package manifest is not a record')
|
|
1220
1387
|
}
|
|
@@ -1236,7 +1403,9 @@ describe('root configuration', () => {
|
|
|
1236
1403
|
it('keeps the committed host inventory aligned with the vendored checkout bytes', async () => {
|
|
1237
1404
|
// Run this gate against a quiescent checkout. It compares two reads and cannot
|
|
1238
1405
|
// distinguish stale committed data from a source edit made while it runs.
|
|
1239
|
-
const packageValue: unknown = JSON.parse(
|
|
1406
|
+
const packageValue: unknown = JSON.parse(
|
|
1407
|
+
readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'),
|
|
1408
|
+
)
|
|
1240
1409
|
if (typeof packageValue !== 'object' || packageValue === null) {
|
|
1241
1410
|
throw new Error('The package manifest is not a record')
|
|
1242
1411
|
}
|
|
@@ -1247,7 +1416,7 @@ describe('root configuration', () => {
|
|
|
1247
1416
|
const generator: unknown = Object.getOwnPropertyDescriptor(scripts, 'build:inventory')?.value
|
|
1248
1417
|
expect(generator === undefined || typeof generator === 'string').toBe(true)
|
|
1249
1418
|
if (generator === undefined) {
|
|
1250
|
-
if (existsSync(resolve(
|
|
1419
|
+
if (existsSync(resolve(POLICY_ROOT, 'host.json'))) {
|
|
1251
1420
|
throw new Error('A committed host inventory exists without a generator')
|
|
1252
1421
|
}
|
|
1253
1422
|
return
|
|
@@ -1255,17 +1424,17 @@ describe('root configuration', () => {
|
|
|
1255
1424
|
if (typeof generator !== 'string') {
|
|
1256
1425
|
throw new Error('The host inventory generator is not a script')
|
|
1257
1426
|
}
|
|
1258
|
-
const committed = resolve(
|
|
1427
|
+
const committed = resolve(POLICY_ROOT, 'host.json')
|
|
1259
1428
|
if (!existsSync(committed)) {
|
|
1260
1429
|
throw new Error('The committed host inventory is absent at host.json')
|
|
1261
1430
|
}
|
|
1262
|
-
const helper = resolve(
|
|
1431
|
+
const helper = resolve(POLICY_ROOT, 'src/server/helpers.ts')
|
|
1263
1432
|
if (!existsSync(helper)) {
|
|
1264
1433
|
throw new Error('The committed host inventory has no server stager')
|
|
1265
1434
|
}
|
|
1266
1435
|
const server = await createServer({
|
|
1267
1436
|
configFile: false,
|
|
1268
|
-
root,
|
|
1437
|
+
root: POLICY_ROOT,
|
|
1269
1438
|
...(configuration.resolve === undefined ? {} : { resolve: configuration.resolve }),
|
|
1270
1439
|
server: { middlewareMode: true },
|
|
1271
1440
|
})
|
|
@@ -1282,49 +1451,47 @@ describe('root configuration', () => {
|
|
|
1282
1451
|
const workspace = scratch.path
|
|
1283
1452
|
try {
|
|
1284
1453
|
const generated = join(workspace, 'host.json')
|
|
1285
|
-
Reflect.apply(stage, undefined, [
|
|
1454
|
+
Reflect.apply(stage, undefined, [POLICY_ROOT, generated])
|
|
1286
1455
|
const generatedText = readFileSync(generated, 'utf8')
|
|
1287
1456
|
const committedText = readFileSync(committed, 'utf8')
|
|
1288
1457
|
const values: readonly unknown[] = [JSON.parse(generatedText), JSON.parse(committedText)]
|
|
1289
|
-
const indexes
|
|
1290
|
-
for (const value of values) {
|
|
1458
|
+
const indexes = values.map((value) => {
|
|
1291
1459
|
if (typeof value !== 'object' || value === null) {
|
|
1292
1460
|
throw new Error('A host inventory is not a record')
|
|
1293
1461
|
}
|
|
1294
1462
|
const entries: unknown = Object.getOwnPropertyDescriptor(value, 'entries')?.value
|
|
1295
1463
|
if (!Array.isArray(entries)) throw new Error('A host inventory carries no entry list')
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
}
|
|
1464
|
+
return new Map(
|
|
1465
|
+
entries.map((entry: unknown): [string, string] => {
|
|
1466
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
1467
|
+
throw new Error('A host inventory carries a malformed entry')
|
|
1468
|
+
}
|
|
1469
|
+
const destination: unknown = Object.getOwnPropertyDescriptor(
|
|
1470
|
+
entry,
|
|
1471
|
+
'destination',
|
|
1472
|
+
)?.value
|
|
1473
|
+
const digest: unknown = Object.getOwnPropertyDescriptor(entry, 'digest')?.value
|
|
1474
|
+
if (typeof destination !== 'string' || typeof digest !== 'string') {
|
|
1475
|
+
throw new Error('A host inventory entry carries no destination digest')
|
|
1476
|
+
}
|
|
1477
|
+
return [destination, digest]
|
|
1478
|
+
}),
|
|
1479
|
+
)
|
|
1480
|
+
})
|
|
1313
1481
|
const generatedIndex = indexes[0]
|
|
1314
1482
|
const committedIndex = indexes[1]
|
|
1315
1483
|
if (generatedIndex === undefined || committedIndex === undefined) {
|
|
1316
1484
|
throw new Error('The host inventories were not indexed')
|
|
1317
1485
|
}
|
|
1318
1486
|
if (generatedText !== committedText) {
|
|
1319
|
-
const stale
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1487
|
+
const stale = [
|
|
1488
|
+
...[...generatedIndex].flatMap(([destination, digest]) =>
|
|
1489
|
+
committedIndex.get(destination) === digest ? [] : [destination],
|
|
1490
|
+
),
|
|
1491
|
+
...[...committedIndex.keys()].filter((destination) => !generatedIndex.has(destination)),
|
|
1492
|
+
]
|
|
1326
1493
|
throw new Error(
|
|
1327
|
-
`The committed host inventory is stale at ${stale.length > 0 ? stale.
|
|
1494
|
+
`The committed host inventory is stale at ${stale.length > 0 ? stale.toSorted().join(', ') : 'host.json'}`,
|
|
1328
1495
|
)
|
|
1329
1496
|
}
|
|
1330
1497
|
console.info(`host-inventory: entries=${generatedIndex.size}`)
|
|
@@ -1337,7 +1504,7 @@ describe('root configuration', () => {
|
|
|
1337
1504
|
})
|
|
1338
1505
|
|
|
1339
1506
|
it('keeps policy rules active across every linted workspace path', () => {
|
|
1340
|
-
const parsed: unknown = JSON.parse(readFileSync(resolve(
|
|
1507
|
+
const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
|
|
1341
1508
|
expect(inspectPolicyConfiguration(parsed)).toEqual([])
|
|
1342
1509
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1343
1510
|
throw new Error('The Oxlint configuration is not a record')
|
|
@@ -1360,7 +1527,7 @@ describe('root configuration', () => {
|
|
|
1360
1527
|
})
|
|
1361
1528
|
|
|
1362
1529
|
it('omits the audit-confirmed dead policy type exports', () => {
|
|
1363
|
-
const source = readFileSync(resolve(
|
|
1530
|
+
const source = readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8')
|
|
1364
1531
|
expect(source).not.toMatch(/\bPolicy(?:Call|ClassMember)\b/u)
|
|
1365
1532
|
})
|
|
1366
1533
|
})
|
|
@@ -2532,6 +2699,85 @@ createSomething({
|
|
|
2532
2699
|
expect(commentToPolicyParagraph({ type: 'Block', value: '* ', range: [0, 0] })).toBe('')
|
|
2533
2700
|
})
|
|
2534
2701
|
|
|
2702
|
+
describe('readers over a caller-supplied list with a hole', () => {
|
|
2703
|
+
// A `for…of` loop and a spread read a hole as `undefined`, where `map`, `filter`, `flatMap`, and
|
|
2704
|
+
// `findLast` skip it or stop before it, so each case puts the hole where the reader would not
|
|
2705
|
+
// reach it and pins the refusal the loop gave.
|
|
2706
|
+
const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
|
|
2707
|
+
const exported: PolicyExpression = {
|
|
2708
|
+
type: 'ExportNamedDeclaration',
|
|
2709
|
+
range: [47, 63],
|
|
2710
|
+
declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
|
|
2711
|
+
}
|
|
2712
|
+
const source: PolicySourceCode = { text: '', getAllComments: () => [] }
|
|
2713
|
+
|
|
2714
|
+
it('refuses a hole in the declarators of a statement as the declarator it reads as', () => {
|
|
2715
|
+
const declarator: PolicyExpression = {
|
|
2716
|
+
type: 'VariableDeclarator',
|
|
2717
|
+
range: [0, 0],
|
|
2718
|
+
id: name,
|
|
2719
|
+
init: { type: 'ArrowFunctionExpression', range: [0, 0] },
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
expect(
|
|
2723
|
+
statementToPolicyBindings({
|
|
2724
|
+
type: 'VariableDeclaration',
|
|
2725
|
+
range: [0, 0],
|
|
2726
|
+
declarations: [declarator],
|
|
2727
|
+
}),
|
|
2728
|
+
).toHaveLength(1)
|
|
2729
|
+
expect(() =>
|
|
2730
|
+
statementToPolicyBindings({
|
|
2731
|
+
type: 'VariableDeclaration',
|
|
2732
|
+
range: [0, 0],
|
|
2733
|
+
declarations: createPolicySparse(2, new Map([[0, declarator]])),
|
|
2734
|
+
}),
|
|
2735
|
+
).toThrow(TypeError)
|
|
2736
|
+
})
|
|
2737
|
+
|
|
2738
|
+
it('refuses a hole in the statements of a program as the statement it reads as', () => {
|
|
2739
|
+
const program: PolicyExpression = {
|
|
2740
|
+
type: 'Program',
|
|
2741
|
+
range: [0, 0],
|
|
2742
|
+
body: createPolicySparse(2, new Map([[0, exported]])),
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
expect(() => programToPolicyDocs(program, source)).toThrow(TypeError)
|
|
2746
|
+
})
|
|
2747
|
+
|
|
2748
|
+
it('refuses a hole in the comments of a source as the comment it reads as', () => {
|
|
2749
|
+
const comment: PolicyComment = { type: 'Block', value: '* One.', range: [0, 11] }
|
|
2750
|
+
const comments = createPolicySparse(2, new Map([[1, comment]]))
|
|
2751
|
+
const program: PolicyExpression = { type: 'Program', range: [0, 0], body: [exported] }
|
|
2752
|
+
|
|
2753
|
+
expect(() =>
|
|
2754
|
+
programToPolicyDocs(program, { text: '/** One. */\n', getAllComments: () => comments }),
|
|
2755
|
+
).toThrow(TypeError)
|
|
2756
|
+
})
|
|
2757
|
+
|
|
2758
|
+
it('refuses a hole in the body of a function-domain module as the statement it reads as', () => {
|
|
2759
|
+
const reports: PolicyDiagnostic[] = []
|
|
2760
|
+
const context: PolicyContext = {
|
|
2761
|
+
filename: '/ws/src/server/execution/run.ts',
|
|
2762
|
+
cwd: '/ws',
|
|
2763
|
+
sourceCode: source,
|
|
2764
|
+
report: (diagnostic) => {
|
|
2765
|
+
reports.push(diagnostic)
|
|
2766
|
+
},
|
|
2767
|
+
}
|
|
2768
|
+
const dense: PolicyExpression = { type: 'Program', range: [0, 0], body: [exported] }
|
|
2769
|
+
const sparse: PolicyExpression = {
|
|
2770
|
+
type: 'Program',
|
|
2771
|
+
range: [0, 0],
|
|
2772
|
+
body: createPolicySparse(2, new Map([[0, exported]])),
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
reportDomain(context, dense)
|
|
2776
|
+
expect(reports).toStrictEqual([])
|
|
2777
|
+
expect(() => reportDomain(context, sparse)).toThrow(TypeError)
|
|
2778
|
+
})
|
|
2779
|
+
})
|
|
2780
|
+
|
|
2535
2781
|
it('reads the opening word of a paragraph as its letters alone', () => {
|
|
2536
2782
|
expect(paragraphToPolicyOpener('Creates a control.')).toBe('Creates')
|
|
2537
2783
|
expect(paragraphToPolicyOpener('"Creates" a control.')).toBe('Creates')
|
|
@@ -2575,27 +2821,99 @@ createSomething({
|
|
|
2575
2821
|
expect(DATA_SOURCE_FILES).not.toContain('plugins.ts')
|
|
2576
2822
|
})
|
|
2577
2823
|
|
|
2824
|
+
it('admits computed ESM imports only in artifact loaders while refusing computed require', () => {
|
|
2825
|
+
const fixture = createPolicyScratch({ prefix: 'artifact-imports-' })
|
|
2826
|
+
try {
|
|
2827
|
+
fixture.write(
|
|
2828
|
+
'configs/policy.ts',
|
|
2829
|
+
readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
|
|
2830
|
+
)
|
|
2831
|
+
const configured = readConfigRecord(
|
|
2832
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
|
|
2833
|
+
)
|
|
2834
|
+
// Include campaign fixtures even when the workspace's default lint walk ignores them.
|
|
2835
|
+
fixture.write('.oxlintrc.json', JSON.stringify({ ...configured, ignorePatterns: [] }))
|
|
2836
|
+
const written = ARTIFACT_IMPORT_CASES.map(([path, admitted]) => {
|
|
2837
|
+
fixture.write(path, 'const selected = process.argv[2]\nvoid import(selected)\ndebugger\n')
|
|
2838
|
+
const required = admitted ? path.replace(/\.(\w+)$/u, '.require.$1') : undefined
|
|
2839
|
+
if (required !== undefined) {
|
|
2840
|
+
fixture.write(
|
|
2841
|
+
required,
|
|
2842
|
+
'const selected = process.argv[2]\nvoid require(selected)\ndebugger\n',
|
|
2843
|
+
)
|
|
2844
|
+
}
|
|
2845
|
+
return { path, required }
|
|
2846
|
+
})
|
|
2847
|
+
const paths = written.map(({ path }) => path)
|
|
2848
|
+
const requires = written.flatMap(({ required }) => (required === undefined ? [] : [required]))
|
|
2849
|
+
const codes = readImportDiagnostics(fixture.path, [...paths, ...requires])
|
|
2850
|
+
for (const [path, admitted] of ARTIFACT_IMPORT_CASES) {
|
|
2851
|
+
expect({ path, refused: codes.has('import(no-dynamic-require) ' + path) }).toEqual({
|
|
2852
|
+
path,
|
|
2853
|
+
refused: !admitted,
|
|
2854
|
+
})
|
|
2855
|
+
expect(codes.has('eslint(no-debugger) ' + path)).toBe(true)
|
|
2856
|
+
}
|
|
2857
|
+
for (const path of requires) {
|
|
2858
|
+
expect(codes.has('import(no-dynamic-require) ' + path)).toBe(true)
|
|
2859
|
+
expect(codes.has('eslint(no-debugger) ' + path)).toBe(true)
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
expect(readConfigRecord(configured.rules)['import/no-dynamic-require']).toEqual([
|
|
2863
|
+
'error',
|
|
2864
|
+
{ esmodule: true },
|
|
2865
|
+
])
|
|
2866
|
+
if (!Array.isArray(configured.overrides)) throw new Error('Missing lint overrides')
|
|
2867
|
+
const overrides = configured.overrides.map(readConfigRecord)
|
|
2868
|
+
const artifact = overrides.filter(
|
|
2869
|
+
(override) => readConfigRecord(override.rules)['import/no-dynamic-require'] !== undefined,
|
|
2870
|
+
)
|
|
2871
|
+
expect(artifact).toEqual([
|
|
2872
|
+
{
|
|
2873
|
+
files: [
|
|
2874
|
+
'src/server/artifacts/**/*.{js,mjs,ts,mts}',
|
|
2875
|
+
'tools/artifacts/**/*.{js,mjs,ts,mts}',
|
|
2876
|
+
'.orkestrel/*/instruments/**/tools/artifacts/**/*.{js,mjs,ts,mts}',
|
|
2877
|
+
],
|
|
2878
|
+
rules: { 'import/no-dynamic-require': ['error', { esmodule: false }] },
|
|
2879
|
+
},
|
|
2880
|
+
])
|
|
2881
|
+
fixture.write(
|
|
2882
|
+
'.oxlintrc.json',
|
|
2883
|
+
JSON.stringify({
|
|
2884
|
+
...configured,
|
|
2885
|
+
ignorePatterns: [],
|
|
2886
|
+
overrides: overrides.filter((override) => !artifact.includes(override)),
|
|
2887
|
+
}),
|
|
2888
|
+
)
|
|
2889
|
+
const original = readImportDiagnostics(fixture.path, [...paths, ...requires])
|
|
2890
|
+
for (const path of [...paths, ...requires]) {
|
|
2891
|
+
expect(original.has('import(no-dynamic-require) ' + path)).toBe(true)
|
|
2892
|
+
}
|
|
2893
|
+
} finally {
|
|
2894
|
+
fixture.destroy()
|
|
2895
|
+
}
|
|
2896
|
+
})
|
|
2897
|
+
|
|
2578
2898
|
it('matches every isolated import pattern with refused and admitted fixtures', () => {
|
|
2579
2899
|
const fixture = createPolicyScratch({ prefix: 'propagation-patterns-' })
|
|
2580
2900
|
const scratch = fixture.path
|
|
2581
2901
|
try {
|
|
2582
2902
|
const configured = readConfigRecord(
|
|
2583
|
-
JSON.parse(readFileSync(resolve(
|
|
2903
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
|
|
2584
2904
|
)
|
|
2585
2905
|
if (!Array.isArray(configured.overrides)) throw new Error('Missing overrides')
|
|
2586
|
-
const overrides:
|
|
2587
|
-
const expected = new Map<string, boolean>()
|
|
2588
|
-
for (const [blockIndex, value] of configured.overrides.entries()) {
|
|
2906
|
+
const built = configured.overrides.flatMap((value: unknown, blockIndex: number) => {
|
|
2589
2907
|
const block = readConfigRecord(value)
|
|
2590
2908
|
const rule = readConfigRecord(block.rules)['no-restricted-imports']
|
|
2591
|
-
if (!Array.isArray(rule))
|
|
2909
|
+
if (!Array.isArray(rule)) return []
|
|
2592
2910
|
const patterns = readConfigRecord(rule[1]).patterns
|
|
2593
2911
|
if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
|
|
2594
|
-
|
|
2912
|
+
return patterns.map((patternValue: unknown, patternIndex: number) => {
|
|
2595
2913
|
const pattern = readConfigRecord(patternValue)
|
|
2596
2914
|
const message = pattern.message
|
|
2597
2915
|
if (typeof message !== 'string') throw new Error('Missing pattern message')
|
|
2598
|
-
|
|
2916
|
+
const listed: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
|
|
2599
2917
|
? [
|
|
2600
2918
|
['https://host/x', true],
|
|
2601
2919
|
['data:text/plain,x', true],
|
|
@@ -2661,49 +2979,59 @@ createSomething({
|
|
|
2661
2979
|
true,
|
|
2662
2980
|
],
|
|
2663
2981
|
]
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
[
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2982
|
+
const face =
|
|
2983
|
+
message.includes('another styles face') && typeof pattern.regex === 'string'
|
|
2984
|
+
? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
|
|
2985
|
+
: undefined
|
|
2986
|
+
if (message.includes('another styles face') && face === undefined)
|
|
2987
|
+
throw new Error('Missing styles face in restriction pattern')
|
|
2988
|
+
const sources: ReadonlyArray<readonly [string, boolean]> =
|
|
2989
|
+
face === undefined
|
|
2990
|
+
? listed
|
|
2991
|
+
: [
|
|
2992
|
+
[`@src/${face}`, true],
|
|
2993
|
+
[`@src/${face}?raw`, true],
|
|
2994
|
+
[`@orkestrel/x/${face}`, true],
|
|
2995
|
+
[`../${face}/index.js`, true],
|
|
2996
|
+
[`../src/${face}/index.js`, true],
|
|
2997
|
+
[`../../src/${face}/index.js`, true],
|
|
2998
|
+
['@src/browser', false],
|
|
2999
|
+
[`@src/${face}x`, false],
|
|
3000
|
+
[`../${face}.ext/index.js`, false],
|
|
3001
|
+
]
|
|
2682
3002
|
const owner =
|
|
2683
3003
|
Array.isArray(block.files) && typeof block.files[0] === 'string'
|
|
2684
3004
|
? block.files[0].split('/**')[0]
|
|
2685
3005
|
: undefined
|
|
2686
3006
|
if (owner === undefined) throw new Error('Missing pattern owner')
|
|
2687
|
-
const
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
3007
|
+
const cases = [...sources, ['./index.js', false] as const].map(
|
|
3008
|
+
([source, refusal], index) => {
|
|
3009
|
+
const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
|
|
3010
|
+
mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
|
|
3011
|
+
writeFileSync(
|
|
3012
|
+
resolve(scratch, path),
|
|
3013
|
+
'import * as boundary from ' +
|
|
3014
|
+
JSON.stringify(source) +
|
|
3015
|
+
'\nvoid boundary\ndebugger\n',
|
|
3016
|
+
)
|
|
3017
|
+
return { path, refusal }
|
|
3018
|
+
},
|
|
3019
|
+
)
|
|
3020
|
+
return {
|
|
3021
|
+
cases,
|
|
3022
|
+
override: {
|
|
3023
|
+
files: cases.map(({ path }) => path),
|
|
3024
|
+
rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
|
|
3025
|
+
},
|
|
2700
3026
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
}
|
|
2706
|
-
|
|
3027
|
+
})
|
|
3028
|
+
})
|
|
3029
|
+
const overrides = built.map(({ override }) => override)
|
|
3030
|
+
const expected = new Map(
|
|
3031
|
+
built.flatMap(({ cases }) =>
|
|
3032
|
+
cases.map(({ path, refusal }): [string, boolean] => [path, refusal]),
|
|
3033
|
+
),
|
|
3034
|
+
)
|
|
2707
3035
|
writeFileSync(
|
|
2708
3036
|
resolve(scratch, '.oxlintrc.json'),
|
|
2709
3037
|
JSON.stringify({ rules: { 'no-debugger': 'error' }, overrides }),
|
|
@@ -2739,132 +3067,154 @@ createSomething({
|
|
|
2739
3067
|
mkdirSync(resolve(scratch, 'configs'))
|
|
2740
3068
|
writeFileSync(
|
|
2741
3069
|
resolve(scratch, 'configs/policy.ts'),
|
|
2742
|
-
readFileSync(resolve(
|
|
3070
|
+
readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts')),
|
|
2743
3071
|
)
|
|
2744
|
-
const text = readFileSync(resolve(
|
|
3072
|
+
const text = readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')
|
|
2745
3073
|
writeFileSync(resolve(scratch, '.oxlintrc.json'), text)
|
|
2746
|
-
const
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
3074
|
+
const faces = ['bootstrap', 'tailwindcss', 'styles']
|
|
3075
|
+
const expected = new Map([
|
|
3076
|
+
...[
|
|
3077
|
+
'src/vue',
|
|
3078
|
+
'app/vue',
|
|
3079
|
+
'src/browser',
|
|
3080
|
+
'src/bootstrap',
|
|
3081
|
+
'src/tailwindcss',
|
|
3082
|
+
'src/styles',
|
|
3083
|
+
'app/browser',
|
|
3084
|
+
'src/core',
|
|
3085
|
+
'app/core',
|
|
3086
|
+
'src/server',
|
|
3087
|
+
'app/server',
|
|
3088
|
+
'src/bin',
|
|
3089
|
+
].flatMap((owner) => {
|
|
3090
|
+
const sheet = ['src/bootstrap', 'src/tailwindcss', 'src/styles'].includes(owner)
|
|
3091
|
+
const refused: ReadonlyArray<string | readonly [string, string]> = [
|
|
3092
|
+
...(owner.endsWith('/vue')
|
|
3093
|
+
? [
|
|
3094
|
+
'node:fs',
|
|
3095
|
+
'@src/server',
|
|
3096
|
+
'../server/index.js',
|
|
3097
|
+
'@src/print',
|
|
3098
|
+
'../print/index.js',
|
|
3099
|
+
...(owner.startsWith('src/')
|
|
3100
|
+
? ['@app/core', '../../app/core/index.js']
|
|
3101
|
+
: ['@app/server', '@app/print']),
|
|
3102
|
+
]
|
|
3103
|
+
: [
|
|
3104
|
+
'vue',
|
|
3105
|
+
'vue/runtime-dom',
|
|
3106
|
+
'@vue/runtime-core',
|
|
3107
|
+
'@vitejs/plugin-vue',
|
|
3108
|
+
'@src/vue',
|
|
3109
|
+
'@orkestrel/sample/vue',
|
|
3110
|
+
'@app/vue',
|
|
3111
|
+
'../vue/index.js',
|
|
3112
|
+
'../../src/vue/index.js',
|
|
3113
|
+
'../../app/vue/index.js',
|
|
3114
|
+
]),
|
|
3115
|
+
...(owner === 'src/core' ? ['@src/browser', './index.css'] : []),
|
|
3116
|
+
...(sheet
|
|
3117
|
+
? [
|
|
3118
|
+
'node:fs',
|
|
3119
|
+
'@src/server',
|
|
3120
|
+
'../server/index.js',
|
|
3121
|
+
'@app/core',
|
|
3122
|
+
...faces
|
|
3123
|
+
.filter((face) => owner !== `src/${face}`)
|
|
3124
|
+
.flatMap((face): ReadonlyArray<string | readonly [string, string]> => [
|
|
3125
|
+
`@src/${face}`,
|
|
3126
|
+
`@orkestrel/x/${face}`,
|
|
3127
|
+
`../${face}/index.js`,
|
|
3128
|
+
`../src/${face}/index.js`,
|
|
3129
|
+
`../../src/${face}/index.js`,
|
|
3130
|
+
['export * from', `@src/${face}`],
|
|
3131
|
+
['import type * as boundary from', `@src/${face}`],
|
|
3132
|
+
]),
|
|
3133
|
+
]
|
|
3134
|
+
: []),
|
|
3135
|
+
]
|
|
3136
|
+
const admitted = [
|
|
3137
|
+
...(owner.endsWith('/vue')
|
|
3138
|
+
? [
|
|
3139
|
+
'@src/core',
|
|
3140
|
+
'@src/browser',
|
|
3141
|
+
'../core/index.js',
|
|
3142
|
+
'../browser/index.js',
|
|
3143
|
+
'vue',
|
|
3144
|
+
'@vue/runtime-core',
|
|
3145
|
+
...(owner.startsWith('app/')
|
|
3146
|
+
? ['@app/core', '@app/browser', '@src/vue', '../../src/vue/index.js']
|
|
3147
|
+
: []),
|
|
3148
|
+
]
|
|
3149
|
+
: ['@src/core']),
|
|
3150
|
+
...(sheet
|
|
3151
|
+
? [
|
|
3152
|
+
...faces
|
|
3153
|
+
.filter((face) => owner !== `src/${face}`)
|
|
3154
|
+
.flatMap((face) => [`@src/${face}x`, `../${face}.ext/index.js`]),
|
|
3155
|
+
'@src/browser',
|
|
3156
|
+
'./sheet.js',
|
|
3157
|
+
'@orkestrel/contract',
|
|
3158
|
+
]
|
|
3159
|
+
: []),
|
|
3160
|
+
]
|
|
3161
|
+
return (
|
|
2817
3162
|
[
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3163
|
+
[
|
|
3164
|
+
[
|
|
3165
|
+
...refused,
|
|
3166
|
+
'https://host/x',
|
|
3167
|
+
'data:text/plain,x',
|
|
3168
|
+
'../core/../server/index.js',
|
|
3169
|
+
'../core/./index.js',
|
|
3170
|
+
],
|
|
3171
|
+
true,
|
|
3172
|
+
],
|
|
3173
|
+
[
|
|
3174
|
+
[
|
|
3175
|
+
...admitted,
|
|
3176
|
+
'./index.js',
|
|
3177
|
+
'../core/index.js',
|
|
3178
|
+
...(owner.endsWith('/server') || owner === 'src/bin' ? ['node:fs'] : []),
|
|
3179
|
+
],
|
|
3180
|
+
false,
|
|
3181
|
+
],
|
|
3182
|
+
] as const
|
|
3183
|
+
).flatMap(([sources, refusal]) =>
|
|
3184
|
+
sources.map((source, index): [string, boolean] => {
|
|
3185
|
+
const file = owner + '/' + (refusal ? 'refused' : 'admitted') + index + '.ts'
|
|
3186
|
+
mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
|
|
3187
|
+
writeFileSync(
|
|
3188
|
+
resolve(scratch, file),
|
|
3189
|
+
typeof source === 'string'
|
|
3190
|
+
? 'import * as boundary from ' +
|
|
3191
|
+
JSON.stringify(source) +
|
|
3192
|
+
'\nvoid boundary\ndebugger\n'
|
|
3193
|
+
: source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
|
|
3194
|
+
)
|
|
3195
|
+
return [file, refusal]
|
|
3196
|
+
}),
|
|
3197
|
+
)
|
|
3198
|
+
}),
|
|
3199
|
+
...(
|
|
2826
3200
|
[
|
|
2827
|
-
[
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
],
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
'\nvoid boundary\ndebugger\n'
|
|
2845
|
-
: source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
|
|
2846
|
-
)
|
|
2847
|
-
expected.set(file, refusal)
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
for (const [file, source, refusal] of [
|
|
2852
|
-
['probe.config.ts', 'typescript', true],
|
|
2853
|
-
['allowed.config.ts', 'node:fs', false],
|
|
2854
|
-
['src/core/compiler.ts', 'typescript', true],
|
|
2855
|
-
['tests/compiler.ts', 'typescript', true],
|
|
2856
|
-
['configs/compiler.ts', 'typescript', true],
|
|
2857
|
-
['scripts/compiler.ts', 'typescript', true],
|
|
2858
|
-
] as const) {
|
|
2859
|
-
mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
|
|
2860
|
-
writeFileSync(
|
|
2861
|
-
resolve(scratch, file),
|
|
2862
|
-
'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
|
|
2863
|
-
)
|
|
2864
|
-
expected.set(file, refusal)
|
|
2865
|
-
}
|
|
2866
|
-
const reports: Array<ReadonlySet<string>> = []
|
|
2867
|
-
for (const disabled of [false, true]) {
|
|
3201
|
+
['probe.config.ts', 'typescript', true],
|
|
3202
|
+
['allowed.config.ts', 'node:fs', false],
|
|
3203
|
+
['src/core/compiler.ts', 'typescript', true],
|
|
3204
|
+
['tests/compiler.ts', 'typescript', true],
|
|
3205
|
+
['configs/compiler.ts', 'typescript', true],
|
|
3206
|
+
['scripts/compiler.ts', 'typescript', true],
|
|
3207
|
+
] as const
|
|
3208
|
+
).map(([file, source, refusal]): [string, boolean] => {
|
|
3209
|
+
mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
|
|
3210
|
+
writeFileSync(
|
|
3211
|
+
resolve(scratch, file),
|
|
3212
|
+
'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
|
|
3213
|
+
)
|
|
3214
|
+
return [file, refusal]
|
|
3215
|
+
}),
|
|
3216
|
+
])
|
|
3217
|
+
const reports = [false, true].map((disabled) => {
|
|
2868
3218
|
if (disabled) {
|
|
2869
3219
|
const parsed: unknown = JSON.parse(text)
|
|
2870
3220
|
if (typeof parsed !== 'object' || parsed === null)
|
|
@@ -2887,9 +3237,8 @@ createSomething({
|
|
|
2887
3237
|
}
|
|
2888
3238
|
writeFileSync(resolve(scratch, '.oxlintrc.json'), JSON.stringify(parsed))
|
|
2889
3239
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
}
|
|
3240
|
+
return readImportDiagnostics(scratch, [...expected.keys()])
|
|
3241
|
+
})
|
|
2893
3242
|
const [enabled, disabled] = reports
|
|
2894
3243
|
if (enabled === undefined || disabled === undefined)
|
|
2895
3244
|
throw new Error('Missing boundary readings')
|
|
@@ -2915,7 +3264,7 @@ createSomething({
|
|
|
2915
3264
|
})
|
|
2916
3265
|
|
|
2917
3266
|
it('enables every plugin rule over the population its law names', () => {
|
|
2918
|
-
const parsed: unknown = JSON.parse(readFileSync(resolve(
|
|
3267
|
+
const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
|
|
2919
3268
|
const rules: unknown = Object.getOwnPropertyDescriptor(policyPlugin, 'rules')?.value
|
|
2920
3269
|
if (typeof rules !== 'object' || rules === null) {
|
|
2921
3270
|
throw new Error('The policy plugin declares no rules')
|
|
@@ -2937,8 +3286,11 @@ createSomething({
|
|
|
2937
3286
|
try {
|
|
2938
3287
|
// Compare diagnostics from the real directory while the child keeps the scratch cwd.
|
|
2939
3288
|
const comparisonRoot = realpathSync.native(scratch.path)
|
|
2940
|
-
scratch.write('.oxlintrc.json', readFileSync(resolve(
|
|
2941
|
-
scratch.write(
|
|
3289
|
+
scratch.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
|
|
3290
|
+
scratch.write(
|
|
3291
|
+
'configs/policy.ts',
|
|
3292
|
+
readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
|
|
3293
|
+
)
|
|
2942
3294
|
scratch.write(
|
|
2943
3295
|
'src/violations/fixture.ts',
|
|
2944
3296
|
[
|
|
@@ -3002,7 +3354,9 @@ createSomething({
|
|
|
3002
3354
|
// extensionless form; spawning the `.cmd` would need `shell: true`, which breaks on paths
|
|
3003
3355
|
// containing spaces. Resolving through `createRequire` reads oxlint's own `bin` field, so
|
|
3004
3356
|
// the entry survives hoisting, a nested `node_modules` layout, and a future rename.
|
|
3005
|
-
const manifestPath = createRequire(join(
|
|
3357
|
+
const manifestPath = createRequire(join(POLICY_ROOT, 'package.json')).resolve(
|
|
3358
|
+
'oxlint/package.json',
|
|
3359
|
+
)
|
|
3006
3360
|
const manifest: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
3007
3361
|
if (typeof manifest !== 'object' || manifest === null) {
|
|
3008
3362
|
throw new Error('The oxlint package manifest is not an object')
|
|
@@ -3029,8 +3383,7 @@ createSomething({
|
|
|
3029
3383
|
[binary, '--config', config, '--format', 'json', 'src/clean'],
|
|
3030
3384
|
{ cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
|
|
3031
3385
|
)
|
|
3032
|
-
const reports: string[]
|
|
3033
|
-
for (const result of [violations, clean]) {
|
|
3386
|
+
const reports = [violations, clean].map((result): string[] => {
|
|
3034
3387
|
if (result.error !== undefined) throw result.error
|
|
3035
3388
|
const report: unknown = JSON.parse(result.stdout)
|
|
3036
3389
|
if (typeof report !== 'object' || report === null) {
|
|
@@ -3038,8 +3391,7 @@ createSomething({
|
|
|
3038
3391
|
}
|
|
3039
3392
|
const diagnostics: unknown = Object.getOwnPropertyDescriptor(report, 'diagnostics')?.value
|
|
3040
3393
|
if (!Array.isArray(diagnostics)) throw new Error('Oxlint returned no diagnostic list')
|
|
3041
|
-
|
|
3042
|
-
for (const diagnostic of diagnostics) {
|
|
3394
|
+
return diagnostics.map((diagnostic: unknown) => {
|
|
3043
3395
|
if (typeof diagnostic !== 'object' || diagnostic === null) {
|
|
3044
3396
|
throw new Error('Oxlint returned a malformed diagnostic')
|
|
3045
3397
|
}
|
|
@@ -3048,10 +3400,9 @@ createSomething({
|
|
|
3048
3400
|
if (typeof code !== 'string' || typeof filename !== 'string') {
|
|
3049
3401
|
throw new Error('Oxlint returned a diagnostic without a rule id and a file')
|
|
3050
3402
|
}
|
|
3051
|
-
|
|
3052
|
-
}
|
|
3053
|
-
|
|
3054
|
-
}
|
|
3403
|
+
return `${code} ${normalizePolicyFilename(comparisonRoot, filename)}`
|
|
3404
|
+
})
|
|
3405
|
+
})
|
|
3055
3406
|
|
|
3056
3407
|
const violationCodes = reports[0]
|
|
3057
3408
|
const cleanCodes = reports[1]
|
|
@@ -3230,9 +3581,11 @@ describe('configuration helpers', () => {
|
|
|
3230
3581
|
|
|
3231
3582
|
it('exposes every helper this proof requires', () => {
|
|
3232
3583
|
const required = [
|
|
3584
|
+
'BuildState',
|
|
3233
3585
|
'ENVIRONMENT_MODULE_BYTES',
|
|
3234
3586
|
'PACKAGE_MANIFEST_BYTES',
|
|
3235
3587
|
'WORKSPACE_ROOT',
|
|
3588
|
+
'attemptValue',
|
|
3236
3589
|
'buildExtractorOverride',
|
|
3237
3590
|
'containedPath',
|
|
3238
3591
|
'decodeAssetSource',
|
|
@@ -3244,6 +3597,7 @@ describe('configuration helpers', () => {
|
|
|
3244
3597
|
'environmentPathError',
|
|
3245
3598
|
'environmentSourceError',
|
|
3246
3599
|
'fileSystemPath',
|
|
3600
|
+
'findPackageDirectory',
|
|
3247
3601
|
'hasAsciiUrlControl',
|
|
3248
3602
|
'isBoundaryExemptModule',
|
|
3249
3603
|
'isExtractorModule',
|
|
@@ -3260,10 +3614,10 @@ describe('configuration helpers', () => {
|
|
|
3260
3614
|
'parseProjectScope',
|
|
3261
3615
|
'physicalPath',
|
|
3262
3616
|
'readBoundedFile',
|
|
3617
|
+
'readHandleBytes',
|
|
3263
3618
|
'readCompilerOutput',
|
|
3264
3619
|
'rewriteCoreSpecifier',
|
|
3265
3620
|
'sourceFallback',
|
|
3266
|
-
'trustedPackageRootFor',
|
|
3267
3621
|
'workspacePath',
|
|
3268
3622
|
]
|
|
3269
3623
|
const found = Object.keys(configHelpers)
|
|
@@ -3302,17 +3656,19 @@ describe('configuration helpers', () => {
|
|
|
3302
3656
|
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-outside-' })
|
|
3303
3657
|
try {
|
|
3304
3658
|
const outside = scratch.path
|
|
3305
|
-
const importer = resolve(
|
|
3306
|
-
expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(
|
|
3307
|
-
expect(configHelpers.fileSystemPath(`/@fs/${
|
|
3308
|
-
expect(configHelpers.physicalPath(`${
|
|
3659
|
+
const importer = resolve(POLICY_ROOT, 'tests/config.test.ts')
|
|
3660
|
+
expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(POLICY_ROOT))
|
|
3661
|
+
expect(configHelpers.fileSystemPath(`/@fs/${POLICY_ROOT}`)).toBe(POLICY_ROOT)
|
|
3662
|
+
expect(configHelpers.physicalPath(`${POLICY_ROOT}?control=true`)).toBe(
|
|
3663
|
+
realpathSync.native(POLICY_ROOT),
|
|
3664
|
+
)
|
|
3309
3665
|
expect(configHelpers.sourceFallback(importer, './setup.ts')).toBe(
|
|
3310
|
-
resolve(
|
|
3666
|
+
resolve(POLICY_ROOT, 'tests/setup.ts'),
|
|
3311
3667
|
)
|
|
3312
3668
|
expect(configHelpers.sourceFallback(importer, pathToFileURL(importer).href)).toBe(importer)
|
|
3313
3669
|
expect(configHelpers.workspacePath(importer)).toBe('tests/config.test.ts')
|
|
3314
|
-
expect(configHelpers.containedPath(
|
|
3315
|
-
expect(configHelpers.containedPath(
|
|
3670
|
+
expect(configHelpers.containedPath(POLICY_ROOT, importer)).toBe(true)
|
|
3671
|
+
expect(configHelpers.containedPath(POLICY_ROOT, outside)).toBe(false)
|
|
3316
3672
|
expect(configHelpers.workspacePath(outside)).toBeUndefined()
|
|
3317
3673
|
expect(configHelpers.isOutsideWorkspacePath(outside)).toBe(true)
|
|
3318
3674
|
expect(configHelpers.isOutsideWorkspacePath('src/core/index.ts')).toBe(false)
|
|
@@ -3351,14 +3707,175 @@ describe('configuration helpers', () => {
|
|
|
3351
3707
|
realpathSync.native(packageRoot),
|
|
3352
3708
|
)
|
|
3353
3709
|
expect(configHelpers.packageRootForResolved(source)).toBe(realpathSync.native(packageRoot))
|
|
3710
|
+
} finally {
|
|
3711
|
+
scratch.destroy()
|
|
3712
|
+
}
|
|
3713
|
+
})
|
|
3714
|
+
|
|
3715
|
+
it('returns a producer value, or undefined when the producer throws', () => {
|
|
3716
|
+
expect(configHelpers.attemptValue(() => decodeURI('%20'))).toBe(' ')
|
|
3717
|
+
expect(configHelpers.attemptValue(() => decodeURI('%E0%A4%A'))).toBeUndefined()
|
|
3718
|
+
expect(configHelpers.attemptValue(() => undefined)).toBeUndefined()
|
|
3719
|
+
expect(configHelpers.attemptValue(() => 0)).toBe(0)
|
|
3720
|
+
})
|
|
3721
|
+
|
|
3722
|
+
it('fills a buffer from an open handle until it ends, and refuses a file over the limit', () => {
|
|
3723
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-handle-' })
|
|
3724
|
+
try {
|
|
3725
|
+
// Each case is a file's text, the offset already read into the buffer, and the largest total allowed.
|
|
3726
|
+
const readings = [
|
|
3727
|
+
{ text: 'abc', offset: 0, limit: 4 },
|
|
3728
|
+
{ text: 'abcd', offset: 0, limit: 4 },
|
|
3729
|
+
{ text: 'abcde', offset: 0, limit: 4 },
|
|
3730
|
+
{ text: '', offset: 0, limit: 4 },
|
|
3731
|
+
{ text: 'abc', offset: 2, limit: 10 },
|
|
3732
|
+
{ text: 'abc', offset: 2, limit: 4 },
|
|
3733
|
+
].map(({ text, offset, limit }) => {
|
|
3734
|
+
scratch.write('subject.txt', text)
|
|
3735
|
+
const handle = openSync(resolve(scratch.path, 'subject.txt'), 'r')
|
|
3736
|
+
try {
|
|
3737
|
+
const bytes = Buffer.alloc(16)
|
|
3738
|
+
const count = configHelpers.readHandleBytes(handle, bytes, offset, limit)
|
|
3739
|
+
return { count, filled: bytes.toString('utf8', offset, count ?? offset) }
|
|
3740
|
+
} finally {
|
|
3741
|
+
closeSync(handle)
|
|
3742
|
+
}
|
|
3743
|
+
})
|
|
3744
|
+
// A file at or under the limit answers its length; the shifted read adds the offset to the
|
|
3745
|
+
// length and fills the buffer past it; a file over the limit answers nothing.
|
|
3746
|
+
expect(readings.map(({ count }) => count)).toStrictEqual([3, 4, undefined, 0, 5, undefined])
|
|
3747
|
+
expect(readings.map(({ filled }) => filled)).toStrictEqual(['abc', 'abcd', '', '', 'abc', ''])
|
|
3748
|
+
} finally {
|
|
3749
|
+
scratch.destroy()
|
|
3750
|
+
}
|
|
3751
|
+
})
|
|
3752
|
+
|
|
3753
|
+
it('finds the nearest package directory from a manifest, a package boundary, or nothing', () => {
|
|
3754
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-find-' })
|
|
3755
|
+
try {
|
|
3756
|
+
scratch.write('held/package.json', '{"name":"held"}')
|
|
3757
|
+
scratch.write('held/src/deep/file.txt', '')
|
|
3758
|
+
scratch.write('modules/node_modules/plain/lib/file.txt', '')
|
|
3759
|
+
scratch.write('modules/node_modules/@scope/named/lib/file.txt', '')
|
|
3760
|
+
scratch.write('modules/node_modules/@scope/file.txt', '')
|
|
3761
|
+
const held = resolve(scratch.path, 'held')
|
|
3762
|
+
expect(configHelpers.findPackageDirectory(held)).toBe(held)
|
|
3763
|
+
expect(configHelpers.findPackageDirectory(resolve(held, 'src/deep'))).toBe(held)
|
|
3764
|
+
const plain = resolve(scratch.path, 'modules/node_modules/plain')
|
|
3765
|
+
expect(configHelpers.findPackageDirectory(resolve(plain, 'lib'))).toBe(plain)
|
|
3766
|
+
const named = resolve(scratch.path, 'modules/node_modules/@scope/named')
|
|
3767
|
+
expect(configHelpers.findPackageDirectory(resolve(named, 'lib'))).toBe(named)
|
|
3768
|
+
expect(configHelpers.findPackageDirectory(named)).toBe(named)
|
|
3769
|
+
// A scope directory alone is not a package boundary, so the walk passes it by.
|
|
3354
3770
|
expect(
|
|
3355
|
-
configHelpers.
|
|
3356
|
-
).toBe(
|
|
3771
|
+
configHelpers.isPackageBoundary(resolve(scratch.path, 'modules/node_modules/@scope')),
|
|
3772
|
+
).toBe(false)
|
|
3773
|
+
// The walk ends at the filesystem root; the answer there is the root only if it holds a manifest.
|
|
3774
|
+
const root = parse(scratch.path).root
|
|
3775
|
+
expect(configHelpers.findPackageDirectory(root)).toBe(
|
|
3776
|
+
existsSync(resolve(root, 'package.json')) ? root : undefined,
|
|
3777
|
+
)
|
|
3778
|
+
} finally {
|
|
3779
|
+
scratch.destroy()
|
|
3780
|
+
}
|
|
3781
|
+
})
|
|
3782
|
+
|
|
3783
|
+
it('holds the workspace defaults until a resolved build config replaces them', async () => {
|
|
3784
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-state-' })
|
|
3785
|
+
try {
|
|
3786
|
+
const fresh = new configHelpers.BuildState()
|
|
3787
|
+
expect(fresh.building).toBe(false)
|
|
3788
|
+
expect(fresh.output).toBe(configHelpers.WORKSPACE_ROOT)
|
|
3789
|
+
expect(fresh.root).toBe(configHelpers.WORKSPACE_ROOT)
|
|
3790
|
+
expect(fresh.source).toBe('')
|
|
3791
|
+
expect(new configHelpers.BuildState('C:/output').output).toBe('C:/output')
|
|
3792
|
+
|
|
3793
|
+
const entry = resolve(scratch.path, 'index.ts')
|
|
3794
|
+
const inline: InlineConfig = {
|
|
3795
|
+
root: scratch.path,
|
|
3796
|
+
configFile: false,
|
|
3797
|
+
logLevel: 'silent',
|
|
3798
|
+
build: { outDir: 'out', lib: { entry, formats: ['es'] } },
|
|
3799
|
+
}
|
|
3800
|
+
const building = new configHelpers.BuildState()
|
|
3801
|
+
building.resolve(await resolveConfig(inline, 'build'))
|
|
3802
|
+
expect(building.building).toBe(true)
|
|
3803
|
+
expect(building.output).toBe(resolve(scratch.path, 'out'))
|
|
3804
|
+
expect(building.root).toBe(realpathSync.native(scratch.path))
|
|
3805
|
+
expect(building.source).toBe(entry)
|
|
3806
|
+
|
|
3807
|
+
// The control: a serve command is not a build, and a config with no library entry has no source.
|
|
3808
|
+
const serving = new configHelpers.BuildState()
|
|
3809
|
+
serving.resolve(await resolveConfig({ ...inline, build: { outDir: 'out' } }, 'serve'))
|
|
3810
|
+
expect(serving.building).toBe(false)
|
|
3811
|
+
expect(serving.source).toBe('')
|
|
3812
|
+
} finally {
|
|
3813
|
+
scratch.destroy()
|
|
3814
|
+
}
|
|
3815
|
+
})
|
|
3816
|
+
|
|
3817
|
+
it('admits each physical package root once and locates the first admitted root containing a path', () => {
|
|
3818
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-locate-' })
|
|
3819
|
+
try {
|
|
3820
|
+
scratch.write('modules/outer/inner/index.ts', '')
|
|
3821
|
+
scratch.write('modules/other/index.ts', '')
|
|
3822
|
+
const outer = realpathSync.native(resolve(scratch.path, 'modules/outer'))
|
|
3823
|
+
const inner = realpathSync.native(resolve(scratch.path, 'modules/outer/inner'))
|
|
3824
|
+
const inside = resolve(scratch.path, 'modules/outer/inner/index.ts')
|
|
3825
|
+
const beside = resolve(scratch.path, 'modules/other/index.ts')
|
|
3826
|
+
|
|
3827
|
+
const state = new configHelpers.BuildState()
|
|
3828
|
+
// A fresh state admits nothing, so it locates nothing.
|
|
3829
|
+
expect(state.locate(inside)).toBeUndefined()
|
|
3830
|
+
state.trust(outer)
|
|
3831
|
+
state.trust(outer)
|
|
3832
|
+
state.trust(inner)
|
|
3833
|
+
expect(state.locate(inside)).toBe(outer)
|
|
3834
|
+
expect(state.locate(beside)).toBeUndefined()
|
|
3835
|
+
// Admission order decides which of two containing roots answers.
|
|
3836
|
+
const reversed = new configHelpers.BuildState()
|
|
3837
|
+
reversed.trust(inner)
|
|
3838
|
+
reversed.trust(outer)
|
|
3839
|
+
expect(reversed.locate(inside)).toBe(inner)
|
|
3840
|
+
expect(new configHelpers.BuildState().locate(inside)).toBeUndefined()
|
|
3841
|
+
// The admitted roots are never returned, so the class publishes no member that lists them.
|
|
3842
|
+
expect(Object.getOwnPropertyNames(configHelpers.BuildState.prototype).sort()).toEqual([
|
|
3843
|
+
'building',
|
|
3844
|
+
'constructor',
|
|
3845
|
+
'locate',
|
|
3846
|
+
'output',
|
|
3847
|
+
'resolve',
|
|
3848
|
+
'root',
|
|
3849
|
+
'source',
|
|
3850
|
+
'trust',
|
|
3851
|
+
])
|
|
3357
3852
|
} finally {
|
|
3358
3853
|
scratch.destroy()
|
|
3359
3854
|
}
|
|
3360
3855
|
})
|
|
3361
3856
|
|
|
3857
|
+
it('climbs from a policy node through its enclosing parentheses alone', () => {
|
|
3858
|
+
const call = { type: 'CallExpression', range: [0, 9] } satisfies PolicyExpression
|
|
3859
|
+
const outer = {
|
|
3860
|
+
type: 'ParenthesizedExpression',
|
|
3861
|
+
range: [1, 8],
|
|
3862
|
+
parent: call,
|
|
3863
|
+
} satisfies PolicyExpression
|
|
3864
|
+
const inner = {
|
|
3865
|
+
type: 'ParenthesizedExpression',
|
|
3866
|
+
range: [2, 7],
|
|
3867
|
+
parent: outer,
|
|
3868
|
+
} satisfies PolicyExpression
|
|
3869
|
+
const leaf = { type: 'Identifier', range: [3, 4], parent: inner } satisfies PolicyExpression
|
|
3870
|
+
expect(nodeToPolicyOuter(leaf)).toBe(outer)
|
|
3871
|
+
expect(nodeToPolicyOuter(inner)).toBe(outer)
|
|
3872
|
+
expect(nodeToPolicyOuter(outer)).toBe(outer)
|
|
3873
|
+
// The control: a node under a non-parenthesized parent, or under none, stays where it is.
|
|
3874
|
+
const argument = { type: 'Identifier', range: [1, 2], parent: call } satisfies PolicyExpression
|
|
3875
|
+
expect(nodeToPolicyOuter(argument)).toBe(argument)
|
|
3876
|
+
expect(nodeToPolicyOuter(call)).toBe(call)
|
|
3877
|
+
})
|
|
3878
|
+
|
|
3362
3879
|
it('classifies module boundaries and extracts static asset sources', async () => {
|
|
3363
3880
|
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-assets-' })
|
|
3364
3881
|
try {
|
|
@@ -3371,8 +3888,12 @@ describe('configuration helpers', () => {
|
|
|
3371
3888
|
expect(configHelpers.hasAsciiUrlControl('clean')).toBe(false)
|
|
3372
3889
|
expect(configHelpers.hasAsciiUrlControl('line\u0000break')).toBe(true)
|
|
3373
3890
|
expect(configHelpers.isBoundaryExemptModule('virtual:control')).toBe(true)
|
|
3374
|
-
expect(configHelpers.isBoundaryExemptModule(resolve(
|
|
3375
|
-
|
|
3891
|
+
expect(configHelpers.isBoundaryExemptModule(resolve(POLICY_ROOT, 'src/core/index.ts'))).toBe(
|
|
3892
|
+
false,
|
|
3893
|
+
)
|
|
3894
|
+
expect(
|
|
3895
|
+
configHelpers.isWorkspaceBoundaryModule(resolve(POLICY_ROOT, 'src/core/index.ts')),
|
|
3896
|
+
).toBe(true)
|
|
3376
3897
|
expect(configHelpers.isWorkspaceBoundaryModule(source)).toBe(false)
|
|
3377
3898
|
expect(configHelpers.isStylesheetPath('src/styles/index.scss?direct')).toBe(true)
|
|
3378
3899
|
expect(configHelpers.isStylesheetPath('src/core/index.ts')).toBe(false)
|
|
@@ -3398,31 +3919,66 @@ describe('configuration helpers', () => {
|
|
|
3398
3919
|
'Browser modules cannot depend on Node or server-only modules',
|
|
3399
3920
|
)
|
|
3400
3921
|
expect(configHelpers.environmentSourceError('src/browser', '@src/core')).toBeUndefined()
|
|
3401
|
-
const expected = resolve(
|
|
3922
|
+
const expected = resolve(POLICY_ROOT, 'dist/config-control')
|
|
3402
3923
|
expect(() => configHelpers.enforceOutputPath(expected, expected)).not.toThrow()
|
|
3403
3924
|
expect(() =>
|
|
3404
|
-
configHelpers.enforceOutputPath(resolve(
|
|
3925
|
+
configHelpers.enforceOutputPath(resolve(POLICY_ROOT, 'dist/outside-control'), expected),
|
|
3405
3926
|
).toThrow('Build output must use its exact configured workspace directory')
|
|
3406
|
-
const outside = resolve(dirname(
|
|
3927
|
+
const outside = resolve(dirname(POLICY_ROOT), 'outside-config-control')
|
|
3407
3928
|
expect(() => configHelpers.enforceOutputPath(outside, outside)).toThrow(
|
|
3408
3929
|
'Build output must remain inside the workspace',
|
|
3409
3930
|
)
|
|
3410
3931
|
})
|
|
3411
3932
|
|
|
3933
|
+
it('admits computed artifact imports through Vite while retaining literal boundary inspection', async () => {
|
|
3934
|
+
for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => admitted)) {
|
|
3935
|
+
await expect(
|
|
3936
|
+
configHelpers.environmentAssetSources(
|
|
3937
|
+
'void import(process.argv[2])',
|
|
3938
|
+
resolve(POLICY_ROOT, path),
|
|
3939
|
+
),
|
|
3940
|
+
).resolves.toEqual([])
|
|
3941
|
+
}
|
|
3942
|
+
for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => !admitted)) {
|
|
3943
|
+
await expect(
|
|
3944
|
+
configHelpers.environmentAssetSources(
|
|
3945
|
+
'void import(process.argv[2])',
|
|
3946
|
+
resolve(POLICY_ROOT, path),
|
|
3947
|
+
),
|
|
3948
|
+
).rejects.toThrow('Dynamic imports must use static string values')
|
|
3949
|
+
}
|
|
3950
|
+
await expect(
|
|
3951
|
+
configHelpers.environmentAssetSources(
|
|
3952
|
+
"void import('@src/browser')",
|
|
3953
|
+
resolve(POLICY_ROOT, 'src/server/artifacts/Artifacts.ts'),
|
|
3954
|
+
),
|
|
3955
|
+
).resolves.toEqual(['@src/browser'])
|
|
3956
|
+
expect(configHelpers.environmentSourceError('src/server', '@src/browser')).toBeDefined()
|
|
3957
|
+
await expect(
|
|
3958
|
+
configHelpers.environmentAssetSources(
|
|
3959
|
+
'void import(process.argv[2])',
|
|
3960
|
+
resolve(POLICY_ROOT, '../outside/src/server/artifacts/Artifacts.ts'),
|
|
3961
|
+
),
|
|
3962
|
+
).rejects.toThrow('Dynamic imports must use static string values')
|
|
3963
|
+
})
|
|
3964
|
+
|
|
3412
3965
|
it('drives each plugin through its real Vite hooks', async () => {
|
|
3413
3966
|
const environments: ReadonlyArray<
|
|
3414
3967
|
'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server'
|
|
3415
3968
|
> = ['src/core', 'src/browser', 'src/server', 'app/core', 'app/browser', 'app/server']
|
|
3416
|
-
const owner = environments.find((environment) => existsSync(resolve(
|
|
3969
|
+
const owner = environments.find((environment) => existsSync(resolve(POLICY_ROOT, environment)))
|
|
3417
3970
|
if (owner === undefined) throw new Error('The workspace carries no environment plugin target')
|
|
3418
|
-
const scratch = createPolicyScratch({
|
|
3971
|
+
const scratch = createPolicyScratch({
|
|
3972
|
+
parent: resolve(POLICY_ROOT, owner),
|
|
3973
|
+
prefix: 'config-build-',
|
|
3974
|
+
})
|
|
3419
3975
|
const workspace = scratch.path
|
|
3420
3976
|
try {
|
|
3421
3977
|
const source = resolve(workspace, 'index.ts')
|
|
3422
3978
|
writeFileSync(source, 'export const control = true\n', 'utf8')
|
|
3423
3979
|
await expect(
|
|
3424
3980
|
build({
|
|
3425
|
-
root,
|
|
3981
|
+
root: POLICY_ROOT,
|
|
3426
3982
|
configFile: false,
|
|
3427
3983
|
logLevel: 'silent',
|
|
3428
3984
|
publicDir: false,
|
|
@@ -3469,7 +4025,7 @@ describe('configuration helpers', () => {
|
|
|
3469
4025
|
// raise its passed count with nothing measured. Every generated workspace runs its projects
|
|
3470
4026
|
// under `--reporter=dot`, which prints a skip as an unnamed `-`, so the case name and the
|
|
3471
4027
|
// condition in it are read by re-running the project with a reporter that names skipped cases.
|
|
3472
|
-
it.skipIf(!
|
|
4028
|
+
it.skipIf(!POLICY_PUBLISHES)(
|
|
3473
4029
|
'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
|
|
3474
4030
|
() => {
|
|
3475
4031
|
const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
|
|
@@ -3477,10 +4033,10 @@ describe('configuration helpers', () => {
|
|
|
3477
4033
|
// no core project, so this walks to the first face the workspace actually carries.
|
|
3478
4034
|
const faces = ['core', 'browser', 'server']
|
|
3479
4035
|
const face = faces.find((candidate) =>
|
|
3480
|
-
existsSync(resolve(
|
|
4036
|
+
existsSync(resolve(POLICY_ROOT, `configs/src/tsconfig.${candidate}.json`)),
|
|
3481
4037
|
)
|
|
3482
4038
|
if (face === undefined) throw new Error('The workspace declares no face project')
|
|
3483
|
-
const project = resolve(
|
|
4039
|
+
const project = resolve(POLICY_ROOT, `configs/src/tsconfig.${face}.json`)
|
|
3484
4040
|
const declared: unknown = JSON.parse(readFileSync(project, 'utf8'))
|
|
3485
4041
|
if (typeof declared !== 'object' || declared === null) {
|
|
3486
4042
|
throw new Error(`The ${face} project is not a TypeScript configuration record`)
|
|
@@ -3535,7 +4091,7 @@ describe('configuration helpers', () => {
|
|
|
3535
4091
|
const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
|
|
3536
4092
|
// Any project path resolves these, because each reading is refused by the text before the
|
|
3537
4093
|
// path is read. The root project is the one file every workspace carries.
|
|
3538
|
-
const project = resolve(
|
|
4094
|
+
const project = resolve(POLICY_ROOT, 'tsconfig.json')
|
|
3539
4095
|
expect(configHelpers.parseProjectScope('not a configuration', project)).toBeUndefined()
|
|
3540
4096
|
expect(
|
|
3541
4097
|
configHelpers.parseProjectScope('{"compilerOptions":{"lib":[],"types":[]}}', project),
|
|
@@ -3548,7 +4104,7 @@ describe('configuration helpers', () => {
|
|
|
3548
4104
|
configHelpers.readCompilerOutput(compiler, [
|
|
3549
4105
|
'--showConfig',
|
|
3550
4106
|
'-p',
|
|
3551
|
-
resolve(
|
|
4107
|
+
resolve(POLICY_ROOT, 'configs/tsconfig.absent.json'),
|
|
3552
4108
|
]),
|
|
3553
4109
|
).toThrow('The declaration compiler failed')
|
|
3554
4110
|
|
|
@@ -3621,12 +4177,12 @@ describe('configuration helpers', () => {
|
|
|
3621
4177
|
|
|
3622
4178
|
// A hoisted install can place the extractor anywhere `require.resolve` reaches, so the skip
|
|
3623
4179
|
// control compares against the path resolution actually returned rather than a fixed layout.
|
|
3624
|
-
it.skipIf(
|
|
3625
|
-
if (
|
|
3626
|
-
expect(existsSync(
|
|
4180
|
+
it.skipIf(POLICY_EXTRACTOR === undefined)('finds the resolved extractor on disk', () => {
|
|
4181
|
+
if (POLICY_EXTRACTOR === undefined) throw new Error('The extractor resolution left no path')
|
|
4182
|
+
expect(existsSync(POLICY_EXTRACTOR)).toBe(true)
|
|
3627
4183
|
})
|
|
3628
4184
|
|
|
3629
|
-
it.skipIf(
|
|
4185
|
+
it.skipIf(POLICY_EXTRACTOR !== undefined)('rejects resolving the unavailable extractor', () => {
|
|
3630
4186
|
expect(() => createRequire(import.meta.url).resolve('@microsoft/api-extractor')).toThrow(
|
|
3631
4187
|
'Cannot find module',
|
|
3632
4188
|
)
|
|
@@ -3634,7 +4190,7 @@ describe('configuration helpers', () => {
|
|
|
3634
4190
|
|
|
3635
4191
|
// The roll-up loads the declaration extractor, which only a workspace publishing source from
|
|
3636
4192
|
// `src` installs. Where `require.resolve` rejects that package, this proof does not apply.
|
|
3637
|
-
it.skipIf(
|
|
4193
|
+
it.skipIf(POLICY_EXTRACTOR === undefined)(
|
|
3638
4194
|
'rolls one face into a single declaration and rewrites its core and browser specifiers',
|
|
3639
4195
|
async () => {
|
|
3640
4196
|
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-rollup-' })
|
|
@@ -3779,3 +4335,133 @@ describe('configuration helpers', () => {
|
|
|
3779
4335
|
},
|
|
3780
4336
|
)
|
|
3781
4337
|
})
|
|
4338
|
+
|
|
4339
|
+
// A `for…of` loop reads one entry, works on it, and stops at the first failure, where a copy of the
|
|
4340
|
+
// list reads every entry first. Each case runs a policy reader over a recorded list and pins the
|
|
4341
|
+
// outcome and the entries it read, as the loop the reader replaced gave them.
|
|
4342
|
+
describe('policy readers consume a caller-supplied list one entry at a time', () => {
|
|
4343
|
+
const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
|
|
4344
|
+
const declarator = (): PolicyExpression => ({
|
|
4345
|
+
type: 'VariableDeclarator',
|
|
4346
|
+
range: [0, 0],
|
|
4347
|
+
id: name,
|
|
4348
|
+
init: { type: 'ArrowFunctionExpression', range: [0, 0] },
|
|
4349
|
+
})
|
|
4350
|
+
const exported = (): PolicyExpression => ({
|
|
4351
|
+
type: 'ExportNamedDeclaration',
|
|
4352
|
+
range: [47, 63],
|
|
4353
|
+
declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
|
|
4354
|
+
})
|
|
4355
|
+
const comment = (): PolicyComment => ({ type: 'Block', value: '* One.', range: [0, 11] })
|
|
4356
|
+
// A revoked proxy refuses every read, which fails the reader once it holds the entry.
|
|
4357
|
+
const refused =
|
|
4358
|
+
<T extends object>(entry: () => T) =>
|
|
4359
|
+
(): T => {
|
|
4360
|
+
const { proxy, revoke } = Proxy.revocable(entry(), {})
|
|
4361
|
+
revoke()
|
|
4362
|
+
return proxy
|
|
4363
|
+
}
|
|
4364
|
+
const throwing = [
|
|
4365
|
+
[POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
|
|
4366
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
|
|
4367
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
|
|
4368
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
|
|
4369
|
+
[POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
|
|
4370
|
+
[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
|
|
4371
|
+
] as const
|
|
4372
|
+
|
|
4373
|
+
it.each([
|
|
4374
|
+
...createPolicyLazyCases(
|
|
4375
|
+
'statementToPolicyBindings',
|
|
4376
|
+
(declarations: PolicyExpression[]) =>
|
|
4377
|
+
statementToPolicyBindings({ type: 'VariableDeclaration', range: [0, 0], declarations }).map(
|
|
4378
|
+
(binding) => binding.name,
|
|
4379
|
+
),
|
|
4380
|
+
{ valid: declarator, refusal: refused(declarator) },
|
|
4381
|
+
[
|
|
4382
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
4383
|
+
[POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
|
|
4384
|
+
...throwing,
|
|
4385
|
+
],
|
|
4386
|
+
),
|
|
4387
|
+
...createPolicyLazyCases(
|
|
4388
|
+
'reportDomain',
|
|
4389
|
+
(body: PolicyExpression[]) => {
|
|
4390
|
+
const reports: Array<string | undefined> = []
|
|
4391
|
+
reportDomain(
|
|
4392
|
+
{
|
|
4393
|
+
filename: '/ws/src/server/execution/run.ts',
|
|
4394
|
+
cwd: '/ws',
|
|
4395
|
+
sourceCode: { text: '', getAllComments: () => [] },
|
|
4396
|
+
report: (diagnostic) => {
|
|
4397
|
+
reports.push(diagnostic.messageId)
|
|
4398
|
+
},
|
|
4399
|
+
},
|
|
4400
|
+
{ type: 'Program', range: [0, 0], body },
|
|
4401
|
+
)
|
|
4402
|
+
return reports
|
|
4403
|
+
},
|
|
4404
|
+
{ valid: exported, refusal: refused(exported) },
|
|
4405
|
+
[
|
|
4406
|
+
[POLICY_LAZY_SHAPES.empty, 'return ["module"]', []],
|
|
4407
|
+
[POLICY_LAZY_SHAPES.dense, 'return []', [0]],
|
|
4408
|
+
...throwing,
|
|
4409
|
+
],
|
|
4410
|
+
),
|
|
4411
|
+
...createPolicyLazyCases(
|
|
4412
|
+
'programToPolicyDocs statements',
|
|
4413
|
+
(body: PolicyExpression[]) =>
|
|
4414
|
+
programToPolicyDocs(
|
|
4415
|
+
{ type: 'Program', range: [0, 0], body },
|
|
4416
|
+
{ text: '/** One. */\nexport function run() {}', getAllComments: () => [comment()] },
|
|
4417
|
+
).map((doc) => doc.name),
|
|
4418
|
+
{ valid: exported, refusal: refused(exported) },
|
|
4419
|
+
[
|
|
4420
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
4421
|
+
[POLICY_LAZY_SHAPES.dense, 'return []', [0]],
|
|
4422
|
+
...throwing,
|
|
4423
|
+
],
|
|
4424
|
+
),
|
|
4425
|
+
...createPolicyLazyCases(
|
|
4426
|
+
'programToPolicyDocs comments',
|
|
4427
|
+
(comments: PolicyComment[]) =>
|
|
4428
|
+
programToPolicyDocs(
|
|
4429
|
+
{ type: 'Program', range: [0, 0], body: [exported()] },
|
|
4430
|
+
{ text: '/** One. */\n', getAllComments: () => comments },
|
|
4431
|
+
).map((doc) => doc.name),
|
|
4432
|
+
{ valid: comment, refusal: refused(comment) },
|
|
4433
|
+
[
|
|
4434
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
4435
|
+
[POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
|
|
4436
|
+
...throwing,
|
|
4437
|
+
],
|
|
4438
|
+
),
|
|
4439
|
+
...createPolicyLazyCases(
|
|
4440
|
+
'programToPolicyDocs comments of a program with no export',
|
|
4441
|
+
(comments: PolicyComment[]) =>
|
|
4442
|
+
programToPolicyDocs(
|
|
4443
|
+
{ type: 'Program', range: [0, 0], body: [{ type: 'ImportDeclaration', range: [0, 0] }] },
|
|
4444
|
+
{ text: 'import x', getAllComments: () => comments },
|
|
4445
|
+
).map((doc) => doc.name),
|
|
4446
|
+
{ valid: comment, refusal: refused(comment) },
|
|
4447
|
+
[
|
|
4448
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
4449
|
+
[POLICY_LAZY_SHAPES.dense, 'return []', []],
|
|
4450
|
+
[POLICY_LAZY_SHAPES.hole, 'return []', []],
|
|
4451
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'return []', []],
|
|
4452
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'return []', []],
|
|
4453
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'return []', []],
|
|
4454
|
+
[POLICY_LAZY_SHAPES.holeBetween, 'return []', []],
|
|
4455
|
+
[POLICY_LAZY_SHAPES.unlisted, 'return []', []],
|
|
4456
|
+
],
|
|
4457
|
+
),
|
|
4458
|
+
])('$label', ({ observe, outcome, reads }) => {
|
|
4459
|
+
const read: number[] = []
|
|
4460
|
+
expect(
|
|
4461
|
+
observe((index) => {
|
|
4462
|
+
read.push(index)
|
|
4463
|
+
}),
|
|
4464
|
+
).toBe(outcome)
|
|
4465
|
+
expect(read).toStrictEqual(reads)
|
|
4466
|
+
})
|
|
4467
|
+
})
|