@orkestrel/scaffold 0.0.81 → 0.0.82
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/bin/main.js +167 -13
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +5 -3
- package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +2 -2
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +41 -38
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +3 -3
- package/dist/host/agents/skills/orkestrel-journey/references/decide.md +3 -3
- package/dist/host/agents/skills/orkestrel-journey/references/layer.md +5 -5
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +3 -3
- package/dist/host/agents/skills/orkestrel-journey/references/styles.md +9 -9
- package/dist/host/claude/rules/application.md +20 -6
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/browser.md +9 -0
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/styles.md +35 -12
- package/dist/host/claude/rules/tests.md +15 -10
- package/dist/host/claude/rules/workspace.md +128 -98
- package/dist/host/claude/skills/orkestrel-journey/SKILL.md +1 -1
- package/dist/host/configs/helpers.ts +157 -9
- package/dist/host/configs/policy.ts +64 -61
- package/dist/host/dotfiles/oxlintrc.json +132 -16
- package/dist/host/dotfiles/prettierignore +1 -1
- package/dist/host/guides/README.md +9 -5
- package/dist/host/guides/scaffold.md +475 -115
- package/dist/host/manifest.json +26 -26
- package/dist/host/tests/config.test.ts +996 -39
- package/dist/host/tests/policy.test.ts +11 -0
- package/dist/host/tests/setupPolicy.ts +396 -22
- package/dist/src/core/index.cjs +1261 -159
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +297 -35
- package/dist/src/core/index.d.ts +297 -35
- package/dist/src/core/index.js +1241 -160
- package/dist/src/core/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
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
5
|
import { spawnSync } from 'node:child_process'
|
|
5
6
|
import {
|
|
6
7
|
existsSync,
|
|
7
8
|
globSync,
|
|
8
9
|
lstatSync,
|
|
9
|
-
mkdtempSync,
|
|
10
10
|
mkdirSync,
|
|
11
11
|
readdirSync,
|
|
12
12
|
readFileSync,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from 'node:fs'
|
|
17
17
|
import { createRequire } from 'node:module'
|
|
18
18
|
import { tmpdir } from 'node:os'
|
|
19
|
-
import { dirname, join, resolve } from 'node:path'
|
|
19
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
21
21
|
import { build, createServer, loadConfigFromFile } from 'vite'
|
|
22
22
|
import { RuleTester } from 'oxlint/plugins-dev'
|
|
@@ -53,8 +53,19 @@ import policyPlugin, {
|
|
|
53
53
|
textToPolicyHits,
|
|
54
54
|
} from '../configs/policy.js'
|
|
55
55
|
import configuration, { resolveWorkspacePath } from '../vite.config.js'
|
|
56
|
+
import * as rootConfiguration from '../vite.config.js'
|
|
56
57
|
import tsconfig from '../tsconfig.json' with { type: 'json' }
|
|
57
58
|
import {
|
|
59
|
+
collectSheets,
|
|
60
|
+
collectFrameworks,
|
|
61
|
+
readConfigRecord,
|
|
62
|
+
readConfigScript,
|
|
63
|
+
collectFaceWrappers,
|
|
64
|
+
inspectSheetConfiguration,
|
|
65
|
+
readSheetPrelude,
|
|
66
|
+
SHEET_POLICY_BARREL_PATTERN,
|
|
67
|
+
SHEET_POLICY_ORDER_PATTERN,
|
|
68
|
+
readImportDiagnostics,
|
|
58
69
|
createPolicyScratch,
|
|
59
70
|
inspectPolicyConfiguration,
|
|
60
71
|
inspectPolicyWiring,
|
|
@@ -64,6 +75,357 @@ import { describe, expect, it } from 'vitest'
|
|
|
64
75
|
|
|
65
76
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
66
77
|
|
|
78
|
+
describe('selected faces', () => {
|
|
79
|
+
it('collects each selected sheet proof from the effective project root and rejects a wrong root', async () => {
|
|
80
|
+
for (const face of collectSheets(root)) {
|
|
81
|
+
const proofs = resolve(root, 'tests/src', face)
|
|
82
|
+
if (!existsSync(proofs)) continue
|
|
83
|
+
const wrapper = resolve(root, `configs/src/vite.${face}.config.ts`)
|
|
84
|
+
const loaded = await loadConfigFromFile(
|
|
85
|
+
{ command: 'serve', mode: 'test' },
|
|
86
|
+
wrapper,
|
|
87
|
+
root,
|
|
88
|
+
'silent',
|
|
89
|
+
)
|
|
90
|
+
if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
|
|
91
|
+
const project = loaded.config
|
|
92
|
+
const include = project.test?.include
|
|
93
|
+
if (include === undefined) throw new Error(`Missing sheet include ${face}`)
|
|
94
|
+
// Vitest roots a file project at its wrapper directory unless test.root overrides it.
|
|
95
|
+
const directory = resolve(
|
|
96
|
+
root,
|
|
97
|
+
project.test?.root ?? dirname(wrapper),
|
|
98
|
+
project.test?.dir || '.',
|
|
99
|
+
)
|
|
100
|
+
const collected = globSync(include, { cwd: directory }).map((path) =>
|
|
101
|
+
resolve(directory, path),
|
|
102
|
+
)
|
|
103
|
+
const expected = [resolve(proofs, 'index.test.ts')]
|
|
104
|
+
if (face === 'styles' && existsSync(resolve(proofs, 'themes/index.test.ts')))
|
|
105
|
+
expected.push(resolve(proofs, 'themes/index.test.ts'))
|
|
106
|
+
expect(collected).toEqual(expect.arrayContaining(expected))
|
|
107
|
+
const control = globSync(include, { cwd: dirname(wrapper) }).map((path) =>
|
|
108
|
+
resolve(dirname(wrapper), path),
|
|
109
|
+
)
|
|
110
|
+
expect(control).toEqual([])
|
|
111
|
+
expect(() => expect(control).toEqual(expect.arrayContaining(expected))).toThrow('expected')
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('enumerates markers independently of wrappers and detects a deleted wrapper', () => {
|
|
116
|
+
const fixture = createPolicyScratch({ prefix: 'propagation-faces-' })
|
|
117
|
+
const scratch = fixture.path
|
|
118
|
+
try {
|
|
119
|
+
expect(collectSheets(scratch)).toEqual([])
|
|
120
|
+
expect(collectFrameworks(scratch)).toEqual([])
|
|
121
|
+
for (const path of [
|
|
122
|
+
'src/paper/index.scss',
|
|
123
|
+
'src/paper/sheet.ts',
|
|
124
|
+
'src/incomplete/index.scss',
|
|
125
|
+
'src/vue/index.ts',
|
|
126
|
+
'app/vue/index.ts',
|
|
127
|
+
]) {
|
|
128
|
+
mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
|
|
129
|
+
writeFileSync(resolve(scratch, path), '')
|
|
130
|
+
}
|
|
131
|
+
expect(collectSheets(scratch)).toEqual(['paper'])
|
|
132
|
+
expect(collectFrameworks(scratch)).toEqual(['src/vue', 'app/vue'])
|
|
133
|
+
for (const path of [
|
|
134
|
+
'configs/src/vite.paper.config.ts',
|
|
135
|
+
'configs/src/vite.vue.config.ts',
|
|
136
|
+
'configs/app/vite.vue.config.ts',
|
|
137
|
+
]) {
|
|
138
|
+
mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
|
|
139
|
+
writeFileSync(resolve(scratch, path), '')
|
|
140
|
+
}
|
|
141
|
+
expect(collectFaceWrappers(scratch)).toContain('configs/src/vite.paper.config.ts')
|
|
142
|
+
rmSync(resolve(scratch, 'configs/src/vite.paper.config.ts'))
|
|
143
|
+
expect(() => collectFaceWrappers(scratch)).toThrow(
|
|
144
|
+
'Missing wrapper configs/src/vite.paper.config.ts',
|
|
145
|
+
)
|
|
146
|
+
expect(collectSheets(scratch)).toContain('paper')
|
|
147
|
+
} finally {
|
|
148
|
+
fixture.destroy()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('loads each selected sheet and framework wrapper and checks its packaging and browser project', async () => {
|
|
153
|
+
const manifest = readConfigRecord(
|
|
154
|
+
JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
|
|
155
|
+
)
|
|
156
|
+
const scripts = readConfigRecord(manifest.scripts)
|
|
157
|
+
const sheets = collectSheets(root)
|
|
158
|
+
const frameworks = collectFrameworks(root)
|
|
159
|
+
const wrappers = collectFaceWrappers(root)
|
|
160
|
+
const readings: Array<{
|
|
161
|
+
readonly axis: string
|
|
162
|
+
readonly face: string
|
|
163
|
+
readonly wrapper: string
|
|
164
|
+
readonly project: UserConfig
|
|
165
|
+
}> = []
|
|
166
|
+
for (const sheet of sheets) expect(wrappers).toContain(`configs/src/vite.${sheet}.config.ts`)
|
|
167
|
+
for (const face of frameworks)
|
|
168
|
+
expect(wrappers).toContain(`configs/${face.replace('/', '/vite.')}.config.ts`)
|
|
169
|
+
for (const wrapper of wrappers) {
|
|
170
|
+
const loaded = await loadConfigFromFile(
|
|
171
|
+
{ command: 'build', mode: 'test' },
|
|
172
|
+
resolve(root, wrapper),
|
|
173
|
+
root,
|
|
174
|
+
'silent',
|
|
175
|
+
)
|
|
176
|
+
if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
|
|
177
|
+
const match = /^configs\/(src|app)\/vite\.([^.]+)\.config\.ts$/u.exec(wrapper)
|
|
178
|
+
const axis = match?.[1]
|
|
179
|
+
const face = match?.[2]
|
|
180
|
+
if (axis === undefined || face === undefined) throw new Error(`Invalid wrapper ${wrapper}`)
|
|
181
|
+
const project = loaded.config
|
|
182
|
+
expect(project.test?.include).toContain(`tests/${axis}/${face}/**/*.test.ts`)
|
|
183
|
+
expect(project.test?.browser).toMatchObject({
|
|
184
|
+
enabled: true,
|
|
185
|
+
instances: expect.arrayContaining([expect.objectContaining({ browser: 'chromium' })]),
|
|
186
|
+
})
|
|
187
|
+
expect(project.optimizeDeps?.include).toEqual(
|
|
188
|
+
expect.arrayContaining(['@orkestrel/test', '@orkestrel/test/browser']),
|
|
189
|
+
)
|
|
190
|
+
expect(
|
|
191
|
+
Object.getOwnPropertyDescriptor(tsconfig.compilerOptions.paths, `@${axis}/${face}`)?.value,
|
|
192
|
+
).toEqual([`./${axis}/${face}/index.ts`])
|
|
193
|
+
readings.push({ axis, face, wrapper, project })
|
|
194
|
+
}
|
|
195
|
+
for (const { face, wrapper, project } of readings.filter((reading) =>
|
|
196
|
+
sheets.includes(reading.face),
|
|
197
|
+
)) {
|
|
198
|
+
expect(readFileSync(resolve(root, wrapper), 'utf8')).toContain('sheetProject(')
|
|
199
|
+
expect(inspectSheetConfiguration(project)).toEqual([])
|
|
200
|
+
const exports = readConfigRecord(manifest.exports)
|
|
201
|
+
expect(exports[`./${face}`]).toBe(`./dist/src/${face}/index.css`)
|
|
202
|
+
expect(exports[`./${face}/scss`]).toBe(`./src/${face}/index.scss`)
|
|
203
|
+
expect(manifest.files).toContain(`src/${face}/**/*.scss`)
|
|
204
|
+
expect(manifest.sideEffects).toEqual(expect.arrayContaining(['**/*.css', '**/*.scss']))
|
|
205
|
+
expect(readConfigScript(scripts, `test:src:${face}`)).toContain(
|
|
206
|
+
`npm run build:src:${face} &&`,
|
|
207
|
+
)
|
|
208
|
+
expect(readConfigScript(scripts, 'build:src')).toContain(`npm run build:src:${face}`)
|
|
209
|
+
}
|
|
210
|
+
for (const { project } of readings.filter((reading) =>
|
|
211
|
+
frameworks.includes(`${reading.axis}/${reading.face}`),
|
|
212
|
+
)) {
|
|
213
|
+
expect(project.optimizeDeps?.include).toContain('vue')
|
|
214
|
+
expect(project.test?.setupFiles).toEqual(
|
|
215
|
+
expect.arrayContaining(['./tests/setup.ts', './tests/setupBrowser.ts']),
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
for (const { wrapper } of readings.filter(
|
|
219
|
+
(reading) => reading.axis === 'src' && frameworks.includes(`${reading.axis}/${reading.face}`),
|
|
220
|
+
)) {
|
|
221
|
+
const text = readFileSync(resolve(root, wrapper), 'utf8')
|
|
222
|
+
expect(text).toContain('rewriteCoreSpecifier')
|
|
223
|
+
expect(text).toContain('rewriteBrowserSpecifier')
|
|
224
|
+
}
|
|
225
|
+
if (!existsSync(resolve(root, 'src/styles/themes/index.scss'))) return
|
|
226
|
+
const loaded = await loadConfigFromFile(
|
|
227
|
+
{ command: 'build', mode: 'test' },
|
|
228
|
+
resolve(root, 'configs/src/vite.themes.config.ts'),
|
|
229
|
+
root,
|
|
230
|
+
'silent',
|
|
231
|
+
)
|
|
232
|
+
if (loaded === null) throw new Error('Unloaded themes wrapper')
|
|
233
|
+
expect(loaded.config.build?.outDir).toBe('dist/src/styles/themes')
|
|
234
|
+
const barrel = readFileSync(resolve(root, 'src/styles/themes/index.scss'), 'utf8')
|
|
235
|
+
expect(readSheetPrelude(barrel)).toMatch(SHEET_POLICY_BARREL_PATTERN)
|
|
236
|
+
expect(
|
|
237
|
+
readSheetPrelude(readFileSync(resolve(root, 'src/styles/_tokens.scss'), 'utf8')),
|
|
238
|
+
).toMatch(SHEET_POLICY_ORDER_PATTERN)
|
|
239
|
+
const target = sheets.includes('styles') ? 'styles' : 'themes'
|
|
240
|
+
const command = sheets.includes('styles')
|
|
241
|
+
? 'vite build --config configs/src/vite.styles.config.ts && vite build --config configs/src/vite.themes.config.ts'
|
|
242
|
+
: 'vite build --config configs/src/vite.themes.config.ts'
|
|
243
|
+
expect(readConfigScript(scripts, `build:src:${target}`)).toBe(command)
|
|
244
|
+
expect(readConfigScript(scripts, 'build:src')).toContain(`npm run build:src:${target}`)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('detects missing sheet setup and reversed themes order in configuration data', () => {
|
|
248
|
+
const project = {
|
|
249
|
+
test: {
|
|
250
|
+
setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts', './tests/setupStyles.ts'],
|
|
251
|
+
isolate: false,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
const script =
|
|
255
|
+
'vite build --config configs/src/vite.styles.config.ts && vite build --config configs/src/vite.themes.config.ts'
|
|
256
|
+
expect(inspectSheetConfiguration(project, script)).toEqual([])
|
|
257
|
+
expect(
|
|
258
|
+
inspectSheetConfiguration(
|
|
259
|
+
{ test: { ...project.test, setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'] } },
|
|
260
|
+
script,
|
|
261
|
+
),
|
|
262
|
+
).toContain('./tests/setupStyles.ts')
|
|
263
|
+
expect(
|
|
264
|
+
inspectSheetConfiguration(project, script.split(' && ').reverse().join(' && ')),
|
|
265
|
+
).toContain('themes order')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('resolves showcase and journey modes for every occupied application', async () => {
|
|
269
|
+
const applications = [
|
|
270
|
+
'browser',
|
|
271
|
+
...collectFrameworks(root)
|
|
272
|
+
.filter((face) => face.startsWith('app/'))
|
|
273
|
+
.map((face) => face.slice(4)),
|
|
274
|
+
]
|
|
275
|
+
const manifest = readConfigRecord(
|
|
276
|
+
JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
|
|
277
|
+
)
|
|
278
|
+
const scripts = readConfigRecord(manifest.scripts)
|
|
279
|
+
const factories = readConfigRecord(rootConfiguration)
|
|
280
|
+
const showcases = ['configs/app/vite.showcase.config.ts'].filter(
|
|
281
|
+
(path) => existsSync(resolve(root, path)) || existsSync(resolve(root, 'showcase')),
|
|
282
|
+
)
|
|
283
|
+
for (const showcase of showcases) {
|
|
284
|
+
const wrapper = readFileSync(resolve(root, 'configs/app/vite.showcase.config.ts'), 'utf8')
|
|
285
|
+
expect(wrapper).toContain('defineConfig(({ mode }) => appShowcase(mode))')
|
|
286
|
+
expect(wrapper).not.toContain('dist/showcase')
|
|
287
|
+
expect(readFileSync(resolve(root, '.prettierignore'), 'utf8').split(/\r\n|\n/u)).toContain(
|
|
288
|
+
'showcase/',
|
|
289
|
+
)
|
|
290
|
+
for (const application of applications) {
|
|
291
|
+
const script = 'build:showcase' + (application === 'browser' ? '' : ':' + application)
|
|
292
|
+
expect(readConfigScript(scripts, script)).toContain('configs/app/vite.showcase.config.ts')
|
|
293
|
+
for (const chain of manifest.private === true
|
|
294
|
+
? []
|
|
295
|
+
: [readConfigScript(scripts, 'prepublishOnly')]) {
|
|
296
|
+
expect(chain).toContain(`npm run ${script}`)
|
|
297
|
+
expect(chain.indexOf('npm run build &&')).toBeLessThan(chain.indexOf(`npm run ${script}`))
|
|
298
|
+
}
|
|
299
|
+
const loaded = await loadConfigFromFile(
|
|
300
|
+
{ command: 'build', mode: application },
|
|
301
|
+
resolve(root, showcase),
|
|
302
|
+
root,
|
|
303
|
+
'silent',
|
|
304
|
+
)
|
|
305
|
+
if (loaded === null) throw new Error('Unloaded showcase wrapper')
|
|
306
|
+
const output = loaded.config.build?.outDir
|
|
307
|
+
if (output === undefined) throw new Error('Missing showcase output')
|
|
308
|
+
expect(relative(root, output)).toBe('showcase')
|
|
309
|
+
expect(() =>
|
|
310
|
+
expect(relative(root, resolve(root, 'dist/showcase'))).toBe('showcase'),
|
|
311
|
+
).toThrow('expected')
|
|
312
|
+
expect(loaded.config.build?.emptyOutDir).toBe(false)
|
|
313
|
+
}
|
|
314
|
+
expect(scripts.show).toBeUndefined()
|
|
315
|
+
}
|
|
316
|
+
if (!existsSync(resolve(root, 'configs/app/vite.journey.config.ts'))) return
|
|
317
|
+
for (const application of applications) {
|
|
318
|
+
const loaded = await loadConfigFromFile(
|
|
319
|
+
{ command: 'serve', mode: application },
|
|
320
|
+
resolve(root, 'configs/app/vite.journey.config.ts'),
|
|
321
|
+
root,
|
|
322
|
+
'silent',
|
|
323
|
+
)
|
|
324
|
+
if (
|
|
325
|
+
loaded === null ||
|
|
326
|
+
!Array.isArray(loaded.config.test?.projects) ||
|
|
327
|
+
loaded.config.test.projects.length === 0
|
|
328
|
+
)
|
|
329
|
+
throw new Error('Missing journey projects')
|
|
330
|
+
const variants = new Set<string>()
|
|
331
|
+
const declared = new Set<string>()
|
|
332
|
+
for (const factory of loaded.config.test.projects) {
|
|
333
|
+
if (typeof factory !== 'function') throw new Error('Missing journey factory')
|
|
334
|
+
const project = readConfigRecord(
|
|
335
|
+
await Reflect.apply(factory, undefined, [{ command: 'serve', mode: application }]),
|
|
336
|
+
)
|
|
337
|
+
const test = readConfigRecord(project.test)
|
|
338
|
+
expect(test.include).toEqual([`tests/app/${application}/integration.test.ts`])
|
|
339
|
+
expect(test.exclude).toEqual([])
|
|
340
|
+
const provide = readConfigRecord(test.provide)
|
|
341
|
+
if (typeof provide.variant !== 'string' || !Array.isArray(provide.variants))
|
|
342
|
+
throw new Error('Missing journey variant values')
|
|
343
|
+
variants.add(provide.variant)
|
|
344
|
+
for (const value of provide.variants) {
|
|
345
|
+
const variant = readConfigRecord(value)
|
|
346
|
+
if (typeof variant.name !== 'string') throw new Error('Unnamed journey variant')
|
|
347
|
+
declared.add(variant.name)
|
|
348
|
+
}
|
|
349
|
+
const selected = provide.variants
|
|
350
|
+
.map(readConfigRecord)
|
|
351
|
+
.find((variant) => variant.name === provide.variant)
|
|
352
|
+
if (selected === undefined) throw new Error('Missing selected variant')
|
|
353
|
+
expect(readConfigRecord(test.browser).viewport).toEqual({
|
|
354
|
+
width: selected.width,
|
|
355
|
+
height: selected.height,
|
|
356
|
+
})
|
|
357
|
+
expect(typeof provide.capture).toBe('boolean')
|
|
358
|
+
expect(readConfigRecord(project.optimizeDeps).include).toEqual(
|
|
359
|
+
expect.arrayContaining(['@orkestrel/test', '@orkestrel/test/browser']),
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
expect(variants).toEqual(declared)
|
|
363
|
+
}
|
|
364
|
+
const factory = factories.appJourney
|
|
365
|
+
if (typeof factory !== 'function') throw new Error('Missing application journey')
|
|
366
|
+
expect(() =>
|
|
367
|
+
Reflect.apply(factory, undefined, [
|
|
368
|
+
{ name: 'desktop', width: 1280, height: 800 },
|
|
369
|
+
[],
|
|
370
|
+
'absent',
|
|
371
|
+
]),
|
|
372
|
+
).toThrow('not declared')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('collects both browser setup proofs and optimizes every selected browser factory', () => {
|
|
376
|
+
const factories = readConfigRecord(rootConfiguration)
|
|
377
|
+
const manifest = readConfigRecord(
|
|
378
|
+
JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
|
|
379
|
+
)
|
|
380
|
+
const dependencies = {
|
|
381
|
+
...readConfigRecord(manifest.dependencies ?? {}),
|
|
382
|
+
...readConfigRecord(manifest.devDependencies ?? {}),
|
|
383
|
+
}
|
|
384
|
+
for (const name of [
|
|
385
|
+
'srcBrowser',
|
|
386
|
+
'appBrowser',
|
|
387
|
+
'srcVue',
|
|
388
|
+
'appVue',
|
|
389
|
+
'setupBrowser',
|
|
390
|
+
'sheetProject',
|
|
391
|
+
'integration',
|
|
392
|
+
]) {
|
|
393
|
+
const factory = factories[name]
|
|
394
|
+
if (typeof factory !== 'function') continue
|
|
395
|
+
const project = readConfigRecord(
|
|
396
|
+
Reflect.apply(factory, undefined, name === 'sheetProject' ? ['src:styles'] : []),
|
|
397
|
+
)
|
|
398
|
+
const test = readConfigRecord(project.test)
|
|
399
|
+
if (readConfigRecord(test.browser ?? {}).enabled !== true) continue
|
|
400
|
+
const include = readConfigRecord(project.optimizeDeps).include
|
|
401
|
+
expect(include).toEqual(
|
|
402
|
+
expect.arrayContaining([
|
|
403
|
+
'@orkestrel/test',
|
|
404
|
+
'@orkestrel/test/browser',
|
|
405
|
+
...(dependencies['@orkestrel/contract'] === undefined ? [] : ['@orkestrel/contract']),
|
|
406
|
+
]),
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
for (const proof of ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts'].filter((path) =>
|
|
410
|
+
existsSync(resolve(root, path)),
|
|
411
|
+
)) {
|
|
412
|
+
const factory = factories.setupBrowser
|
|
413
|
+
if (typeof factory !== 'function') throw new Error('Missing browser setup project')
|
|
414
|
+
expect(
|
|
415
|
+
readConfigRecord(readConfigRecord(Reflect.apply(factory, undefined, [])).test).include,
|
|
416
|
+
).toContain(proof)
|
|
417
|
+
expect(
|
|
418
|
+
readConfigRecord(readConfigRecord(Reflect.apply(factory, undefined, [])).test).include,
|
|
419
|
+
).toEqual(expect.arrayContaining(['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts']))
|
|
420
|
+
}
|
|
421
|
+
if (typeof factories.setup !== 'function') return
|
|
422
|
+
expect(
|
|
423
|
+
readConfigRecord(readConfigRecord(Reflect.apply(factories.setup, undefined, [])).test)
|
|
424
|
+
.exclude,
|
|
425
|
+
).toEqual(expect.arrayContaining(['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts']))
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
67
429
|
// The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
|
|
68
430
|
// a physical directory rather than from a `src` entry of any shape. A regular file, an empty
|
|
69
431
|
// directory, a directory holding no recognized environment, a symbolic link, and an entry this host
|
|
@@ -113,8 +475,15 @@ describe('root configuration', () => {
|
|
|
113
475
|
if (existsSync(resolve(root, path))) required.set(`@${axis}/${environment}`, path)
|
|
114
476
|
}
|
|
115
477
|
}
|
|
116
|
-
|
|
478
|
+
for (const face of collectSheets(root)) required.set(`@src/${face}`, `src/${face}/index.ts`)
|
|
479
|
+
for (const face of collectFrameworks(root)) required.set(`@${face}`, `${face}/index.ts`)
|
|
117
480
|
const declared = new Map(Object.entries(tsconfig.compilerOptions.paths))
|
|
481
|
+
expect(required.size > 0 || existsSync(resolve(root, 'src/styles/themes/sheet.ts'))).toBe(true)
|
|
482
|
+
if (required.size === 0) {
|
|
483
|
+
if (declared.size !== 0 || Object.keys(aliases).length !== 0)
|
|
484
|
+
throw new Error('The themes-only workspace declares an unexpected alias')
|
|
485
|
+
return
|
|
486
|
+
}
|
|
118
487
|
const absent = new Map<string, readonly string[]>()
|
|
119
488
|
expect(() => {
|
|
120
489
|
for (const key of required.keys()) {
|
|
@@ -138,7 +507,7 @@ describe('root configuration', () => {
|
|
|
138
507
|
string,
|
|
139
508
|
{
|
|
140
509
|
readonly benchmark?: readonly string[]
|
|
141
|
-
readonly include: string
|
|
510
|
+
readonly include: string | readonly string[]
|
|
142
511
|
readonly parallel?: boolean
|
|
143
512
|
readonly pool?: string
|
|
144
513
|
readonly setup: readonly string[]
|
|
@@ -203,21 +572,35 @@ describe('root configuration', () => {
|
|
|
203
572
|
'integration',
|
|
204
573
|
]) {
|
|
205
574
|
if (!existsSync(resolve(root, `tests/${label}.test.ts`))) continue
|
|
575
|
+
const setup = ['./tests/setup.ts']
|
|
576
|
+
if (label === 'conformance') setup.push('./tests/setupServer.ts')
|
|
577
|
+
if (
|
|
578
|
+
label === 'integration' &&
|
|
579
|
+
(collectSheets(root).length > 0 ||
|
|
580
|
+
existsSync(resolve(root, 'src/styles/themes/index.scss')))
|
|
581
|
+
)
|
|
582
|
+
setup.push('./tests/setupBrowser.ts', './tests/setupStyles.ts')
|
|
206
583
|
expected.set(label, {
|
|
207
584
|
include: `tests/${label}.test.ts`,
|
|
208
|
-
setup
|
|
585
|
+
setup,
|
|
209
586
|
})
|
|
210
587
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
588
|
+
const browserProofs = ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts']
|
|
589
|
+
const setupProofs = globSync('tests/setup*.test.ts', { cwd: root })
|
|
590
|
+
.map((path) => path.replaceAll('\\', '/'))
|
|
591
|
+
.filter((path) => /^tests\/setup[^/]*\.test\.ts$/u.test(path))
|
|
592
|
+
if (setupProofs.some((path) => !browserProofs.includes(path))) {
|
|
216
593
|
expected.set('setup', {
|
|
217
594
|
include: 'tests/setup*.test.ts',
|
|
218
595
|
setup: ['./tests/setup.ts'],
|
|
219
596
|
})
|
|
220
597
|
}
|
|
598
|
+
if (setupProofs.some((path) => browserProofs.includes(path))) {
|
|
599
|
+
expected.set('setup:browser', {
|
|
600
|
+
include: browserProofs,
|
|
601
|
+
setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
602
|
+
})
|
|
603
|
+
}
|
|
221
604
|
// The live-service project covers a directory rather than one proof, so its
|
|
222
605
|
// readiness module is the fact that selects it. A suite beneath
|
|
223
606
|
// `tests/service` with no setup module is a project nothing configures.
|
|
@@ -269,7 +652,7 @@ describe('root configuration', () => {
|
|
|
269
652
|
string,
|
|
270
653
|
{
|
|
271
654
|
readonly benchmark?: readonly string[]
|
|
272
|
-
readonly include: string
|
|
655
|
+
readonly include: string | readonly string[]
|
|
273
656
|
readonly parallel?: boolean
|
|
274
657
|
readonly pool?: string
|
|
275
658
|
readonly setup: readonly string[]
|
|
@@ -323,9 +706,14 @@ describe('root configuration', () => {
|
|
|
323
706
|
const effective = include.filter(
|
|
324
707
|
(path) => typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
|
|
325
708
|
)
|
|
326
|
-
if (
|
|
327
|
-
|
|
709
|
+
if (
|
|
710
|
+
effective.length !== (label === 'setup:browser' ? 2 : 1) ||
|
|
711
|
+
typeof effective[0] !== 'string'
|
|
712
|
+
) {
|
|
713
|
+
throw new Error(`${label} does not resolve to its required includes`)
|
|
328
714
|
}
|
|
715
|
+
for (const required of label === 'setup' ? browserProofs : [])
|
|
716
|
+
expect(exclude).toContain(required)
|
|
329
717
|
if (label === 'probe') {
|
|
330
718
|
const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
|
|
331
719
|
const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
|
|
@@ -354,13 +742,28 @@ describe('root configuration', () => {
|
|
|
354
742
|
})
|
|
355
743
|
continue
|
|
356
744
|
}
|
|
357
|
-
configured.set(label, {
|
|
745
|
+
configured.set(label, {
|
|
746
|
+
include: label === 'setup:browser' ? effective : effective[0],
|
|
747
|
+
setup: [...new Set(setup)],
|
|
748
|
+
})
|
|
358
749
|
}
|
|
359
750
|
|
|
360
751
|
// Required projects come from present source and test paths. Each factory is selected by name
|
|
361
752
|
// before its result is read. Extra factories are ignored without validating their result shape.
|
|
362
753
|
expect(extraLoaded).toBe(false)
|
|
363
754
|
for (const [label, project] of expected) expect(configured.get(label)).toStrictEqual(project)
|
|
755
|
+
for (const label of ['setup', 'setup:browser']) {
|
|
756
|
+
const name = label === 'setup' ? 'setup' : 'setupBrowser'
|
|
757
|
+
expect(projects.some((entry) => typeof entry === 'function' && entry.name === name)).toBe(
|
|
758
|
+
expected.has(label),
|
|
759
|
+
)
|
|
760
|
+
if (!expected.has(label)) continue
|
|
761
|
+
const missingSetup = new Map(configured)
|
|
762
|
+
missingSetup.delete(label)
|
|
763
|
+
expect(() => expect(missingSetup.get(label)).toStrictEqual(expected.get(label))).toThrow(
|
|
764
|
+
'expected',
|
|
765
|
+
)
|
|
766
|
+
}
|
|
364
767
|
|
|
365
768
|
const missing = new Map(configured)
|
|
366
769
|
expect(missing.delete('probe')).toBe(true)
|
|
@@ -378,7 +781,7 @@ describe('root configuration', () => {
|
|
|
378
781
|
}).toThrow(/strictly equal/u)
|
|
379
782
|
})
|
|
380
783
|
|
|
381
|
-
it('
|
|
784
|
+
it('loads file projects and returns only the invocation mode from registered project factories', async () => {
|
|
382
785
|
const projects = configuration.test?.projects
|
|
383
786
|
if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
|
|
384
787
|
if (projects.length === 0) throw new Error('The root configuration registers no project')
|
|
@@ -402,7 +805,32 @@ describe('root configuration', () => {
|
|
|
402
805
|
const spreading = Object.defineProperty((record: object) => ({ ...record, test }), 'name', {
|
|
403
806
|
value: 'spreading',
|
|
404
807
|
})
|
|
405
|
-
const
|
|
808
|
+
const wrappers = projects.filter((entry) => typeof entry === 'string')
|
|
809
|
+
const sheets = collectSheets(root)
|
|
810
|
+
const expectedWrappers = sheets.map((face) => `./configs/src/vite.${face}.config.ts`)
|
|
811
|
+
if (!sheets.includes('styles') && existsSync(resolve(root, 'src/styles/themes/index.scss')))
|
|
812
|
+
expectedWrappers.push('./configs/src/vite.themes.config.ts')
|
|
813
|
+
expect([...wrappers].sort()).toEqual(expectedWrappers.sort())
|
|
814
|
+
for (const wrapper of wrappers) {
|
|
815
|
+
const loaded = await loadConfigFromFile(
|
|
816
|
+
{ command: 'serve', mode: invocation.mode },
|
|
817
|
+
resolve(root, wrapper),
|
|
818
|
+
root,
|
|
819
|
+
'silent',
|
|
820
|
+
)
|
|
821
|
+
if (loaded === null) throw new Error(`Unloaded file project ${wrapper}`)
|
|
822
|
+
const face = /^\.\/configs\/src\/vite\.([^.]+)\.config\.ts$/u.exec(wrapper)?.[1]
|
|
823
|
+
if (face === undefined) throw new Error(`Invalid file project ${wrapper}`)
|
|
824
|
+
expect(loaded.config.test?.name).toMatchObject({ label: `src:${face}` })
|
|
825
|
+
expect(loaded.config.test?.include).toEqual([
|
|
826
|
+
`tests/src/${face === 'themes' ? 'styles/themes' : face}/**/*.test.ts`,
|
|
827
|
+
])
|
|
828
|
+
expect(() =>
|
|
829
|
+
expect({ test: {} }).toMatchObject({ test: { name: { label: `src:${face}` } } }),
|
|
830
|
+
).toThrow('expected')
|
|
831
|
+
}
|
|
832
|
+
const factories = projects.filter((entry) => typeof entry !== 'string')
|
|
833
|
+
const entries: readonly unknown[] = [...factories, ignoring, spreading, { test }]
|
|
406
834
|
const readings = entries.map((entry) => {
|
|
407
835
|
if (typeof entry !== 'function') {
|
|
408
836
|
return { name: undefined, callable: false, mode: undefined, leaked: [] }
|
|
@@ -421,13 +849,13 @@ describe('root configuration', () => {
|
|
|
421
849
|
}
|
|
422
850
|
})
|
|
423
851
|
const forwarded = { callable: true, mode: 'sentinel-mode', leaked: [] }
|
|
424
|
-
expect(readings.slice(0,
|
|
425
|
-
|
|
852
|
+
expect(readings.slice(0, factories.length)).toStrictEqual(
|
|
853
|
+
factories.map((entry) => ({
|
|
426
854
|
name: typeof entry === 'function' ? entry.name : undefined,
|
|
427
855
|
...forwarded,
|
|
428
856
|
})),
|
|
429
857
|
)
|
|
430
|
-
expect(readings.slice(
|
|
858
|
+
expect(readings.slice(factories.length)).toStrictEqual([
|
|
431
859
|
{ name: 'ignoring', callable: true, mode: undefined, leaked: [] },
|
|
432
860
|
{
|
|
433
861
|
name: 'spreading',
|
|
@@ -437,7 +865,7 @@ describe('root configuration', () => {
|
|
|
437
865
|
},
|
|
438
866
|
{ name: undefined, callable: false, mode: undefined, leaked: [] },
|
|
439
867
|
])
|
|
440
|
-
for (const reading of readings.slice(
|
|
868
|
+
for (const reading of readings.slice(factories.length)) {
|
|
441
869
|
expect({ ...reading, name: undefined }).not.toStrictEqual({ name: undefined, ...forwarded })
|
|
442
870
|
}
|
|
443
871
|
|
|
@@ -462,6 +890,9 @@ describe('root configuration', () => {
|
|
|
462
890
|
}
|
|
463
891
|
}
|
|
464
892
|
}
|
|
893
|
+
for (const face of collectFrameworks(root)) {
|
|
894
|
+
required.push(`configs/${face.replace('/', '/tsconfig.')}.json`)
|
|
895
|
+
}
|
|
465
896
|
if (existsSync(resolve(root, 'src/bin'))) {
|
|
466
897
|
required.push('configs/src/tsconfig.bin.json', 'configs/src/vite.bin.config.ts')
|
|
467
898
|
}
|
|
@@ -558,7 +989,7 @@ describe('root configuration', () => {
|
|
|
558
989
|
environment === 'bin'
|
|
559
990
|
? 'dist/bin'
|
|
560
991
|
: environment === 'showcase'
|
|
561
|
-
? '
|
|
992
|
+
? 'showcase'
|
|
562
993
|
: `dist/${axis}/${environment}`
|
|
563
994
|
if (resolve(root, output) !== resolve(root, expected)) {
|
|
564
995
|
throw new Error(`${wrapper} resolves to the wrong output`)
|
|
@@ -567,7 +998,7 @@ describe('root configuration', () => {
|
|
|
567
998
|
}
|
|
568
999
|
|
|
569
1000
|
const tsconfigMatch =
|
|
570
|
-
/^configs\/(src|app)\/tsconfig\.(core|browser|server|bin)\.json$/u.exec(wrapper)
|
|
1001
|
+
/^configs\/(src|app)\/tsconfig\.(core|browser|server|bin|vue)\.json$/u.exec(wrapper)
|
|
571
1002
|
if (tsconfigMatch === null) {
|
|
572
1003
|
throw new Error(`${wrapper} is not a required target wrapper`)
|
|
573
1004
|
}
|
|
@@ -591,19 +1022,22 @@ describe('root configuration', () => {
|
|
|
591
1022
|
const expectedLib =
|
|
592
1023
|
environment === 'core'
|
|
593
1024
|
? ['ESNext', 'WebWorker']
|
|
594
|
-
: environment === 'browser'
|
|
1025
|
+
: environment === 'browser' || environment === 'vue'
|
|
595
1026
|
? ['ESNext', 'DOM', 'DOM.Iterable']
|
|
596
1027
|
: ['ESNext']
|
|
597
1028
|
const expectedTypes =
|
|
598
1029
|
environment === 'core'
|
|
599
1030
|
? []
|
|
600
|
-
: environment === 'browser'
|
|
601
|
-
? axis === 'app'
|
|
1031
|
+
: environment === 'browser' || environment === 'vue'
|
|
1032
|
+
? axis === 'app' && environment === 'vue'
|
|
602
1033
|
? ['vite/client', 'vue']
|
|
603
1034
|
: ['vite/client']
|
|
604
1035
|
: ['node']
|
|
605
1036
|
expect(lib).toStrictEqual(expectedLib)
|
|
606
1037
|
expect(types).toStrictEqual(expectedTypes)
|
|
1038
|
+
for (const control of axis === 'app' && environment === 'vue' ? [['vite/client']] : []) {
|
|
1039
|
+
expect(() => expect(control).toStrictEqual(expectedTypes)).toThrow('expected')
|
|
1040
|
+
}
|
|
607
1041
|
}
|
|
608
1042
|
for (const tests of journeys) {
|
|
609
1043
|
const names: string[] = []
|
|
@@ -827,7 +1261,8 @@ describe('root configuration', () => {
|
|
|
827
1261
|
if (typeof stage !== 'function') {
|
|
828
1262
|
throw new Error('The server helper module exports no stageInventory function')
|
|
829
1263
|
}
|
|
830
|
-
const
|
|
1264
|
+
const scratch = createPolicyScratch({ prefix: 'host-inventory-' })
|
|
1265
|
+
const workspace = scratch.path
|
|
831
1266
|
try {
|
|
832
1267
|
const generated = join(workspace, 'host.json')
|
|
833
1268
|
Reflect.apply(stage, undefined, [root, generated])
|
|
@@ -877,7 +1312,7 @@ describe('root configuration', () => {
|
|
|
877
1312
|
}
|
|
878
1313
|
console.info(`host-inventory: entries=${generatedIndex.size}`)
|
|
879
1314
|
} finally {
|
|
880
|
-
|
|
1315
|
+
scratch.destroy()
|
|
881
1316
|
}
|
|
882
1317
|
} finally {
|
|
883
1318
|
await server.close()
|
|
@@ -1005,7 +1440,7 @@ describe('policy plugin', () => {
|
|
|
1005
1440
|
code: 'function createProjector() { return () => 1 }',
|
|
1006
1441
|
},
|
|
1007
1442
|
{
|
|
1008
|
-
name: 'accepts
|
|
1443
|
+
name: 'accepts callbacks in a returned object literal',
|
|
1009
1444
|
code: [
|
|
1010
1445
|
'function reportNode(context, node) { context.report({ node }) }',
|
|
1011
1446
|
'const RULE = {',
|
|
@@ -1023,6 +1458,45 @@ describe('policy plugin', () => {
|
|
|
1023
1458
|
name: 'accepts class accessors inside a factory',
|
|
1024
1459
|
code: 'function createAccessor() { class Accessor { get value() { return 1 } set value(value) { consume(value) } } return Accessor }',
|
|
1025
1460
|
},
|
|
1461
|
+
{
|
|
1462
|
+
name: 'accepts the event-map option in a function body',
|
|
1463
|
+
code: `function configure() {
|
|
1464
|
+
createSomething({
|
|
1465
|
+
on: {
|
|
1466
|
+
thing: function that() {
|
|
1467
|
+
console.log('that thing')
|
|
1468
|
+
},
|
|
1469
|
+
other: () => {
|
|
1470
|
+
console.log('this other')
|
|
1471
|
+
},
|
|
1472
|
+
},
|
|
1473
|
+
})
|
|
1474
|
+
}`,
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
name: 'accepts an array element in a call argument',
|
|
1478
|
+
code: 'function configure() { return create([() => 1]) }',
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
name: 'accepts methods and accessors in admitted object positions',
|
|
1482
|
+
code: 'function configure() { create({ on: { thing() { return 1 } } }); new Factory({ get value() { return 1 }, set value(value) { consume(value) } }); return { read() { return 1 } } }',
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
name: 'accepts an object member returned from a function',
|
|
1486
|
+
code: 'function configure() { return { read: function readValue() { return 1 } } }',
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
name: 'accepts an object member in a parenthesized arrow body',
|
|
1490
|
+
code: 'const configure = () => ({ read: () => 1 })',
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
name: 'accepts a parenthesized member in a constructor argument',
|
|
1494
|
+
code: 'function configure() { return new Factory({ read: (() => 1) }) }',
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
name: 'accepts a named function expression argument',
|
|
1498
|
+
code: 'function projectValue() { return read(function readValue() { return 1 }) }',
|
|
1499
|
+
},
|
|
1026
1500
|
],
|
|
1027
1501
|
invalid: [
|
|
1028
1502
|
{
|
|
@@ -1054,11 +1528,6 @@ describe('policy plugin', () => {
|
|
|
1054
1528
|
code: 'function projectValue() { const readValue = () => 1; return readValue() }',
|
|
1055
1529
|
errors: [{ messageId: 'nested' }],
|
|
1056
1530
|
},
|
|
1057
|
-
{
|
|
1058
|
-
name: 'rejects a named function expression argument',
|
|
1059
|
-
code: 'function projectValue() { return read(function readValue() { return 1 }) }',
|
|
1060
|
-
errors: [{ messageId: 'nested' }],
|
|
1061
|
-
},
|
|
1062
1531
|
{
|
|
1063
1532
|
name: 'rejects a callback parameter default function',
|
|
1064
1533
|
code: 'function projectValue() { return values.map((value = () => 1) => value()) }',
|
|
@@ -1074,6 +1543,48 @@ describe('policy plugin', () => {
|
|
|
1074
1543
|
code: 'class Project { read() { const value = () => 1; return value() } }',
|
|
1075
1544
|
errors: [{ messageId: 'nested' }],
|
|
1076
1545
|
},
|
|
1546
|
+
{
|
|
1547
|
+
name: 'rejects an object literal bound locally before being passed',
|
|
1548
|
+
code: 'function configure() { const options = { on: { thing: () => 1 } }; return create(options) }',
|
|
1549
|
+
errors: [{ messageId: 'nested' }],
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
name: 'rejects an object method bound locally before being passed',
|
|
1553
|
+
code: 'function configure() { const options = { on: { thing() { return 1 } } }; return create(options) }',
|
|
1554
|
+
errors: [{ messageId: 'nested' }],
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
name: 'rejects object accessors bound locally before being passed',
|
|
1558
|
+
code: 'function configure() { const options = { get value() { return 1 }, set value(value) { consume(value) } }; return create(options) }',
|
|
1559
|
+
errors: [{ messageId: 'nested' }, { messageId: 'nested' }],
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
name: 'rejects an object member reached through a spread',
|
|
1563
|
+
code: 'function configure() { return create({ ...{ thing: () => 1 } }) }',
|
|
1564
|
+
errors: [{ messageId: 'nested' }],
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
name: 'rejects a computed property value',
|
|
1568
|
+
code: 'function configure() { return create({ [key]: () => 1 }) }',
|
|
1569
|
+
errors: [{ messageId: 'nested' }],
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
name: 'rejects a local binding inside a getter in an argument',
|
|
1573
|
+
code: 'function configure() { return create({ get value() { const read = () => 1; return read() } }) }',
|
|
1574
|
+
errors: [{ messageId: 'nested' }],
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
name: 'rejects only the local binding inside an admitted member',
|
|
1578
|
+
code: [
|
|
1579
|
+
'function configure() {',
|
|
1580
|
+
'create({ on: { thing: () => {',
|
|
1581
|
+
'const read = () => 1;',
|
|
1582
|
+
'return read()',
|
|
1583
|
+
'} } })',
|
|
1584
|
+
'}',
|
|
1585
|
+
].join('\n'),
|
|
1586
|
+
errors: [{ messageId: 'nested', line: 3, column: 13 }],
|
|
1587
|
+
},
|
|
1077
1588
|
],
|
|
1078
1589
|
})
|
|
1079
1590
|
|
|
@@ -1874,6 +2385,302 @@ describe('policy plugin', () => {
|
|
|
1874
2385
|
expect(CENTRAL_SOURCE_FILES).toContain('handlers.ts')
|
|
1875
2386
|
})
|
|
1876
2387
|
|
|
2388
|
+
it('matches every isolated import pattern with refused and admitted fixtures', () => {
|
|
2389
|
+
const fixture = createPolicyScratch({ prefix: 'propagation-patterns-' })
|
|
2390
|
+
const scratch = fixture.path
|
|
2391
|
+
try {
|
|
2392
|
+
const configured = readConfigRecord(
|
|
2393
|
+
JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')),
|
|
2394
|
+
)
|
|
2395
|
+
if (!Array.isArray(configured.overrides)) throw new Error('Missing overrides')
|
|
2396
|
+
const overrides: object[] = []
|
|
2397
|
+
const expected = new Map<string, boolean>()
|
|
2398
|
+
for (const [blockIndex, value] of configured.overrides.entries()) {
|
|
2399
|
+
const block = readConfigRecord(value)
|
|
2400
|
+
const rule = readConfigRecord(block.rules)['no-restricted-imports']
|
|
2401
|
+
if (!Array.isArray(rule)) continue
|
|
2402
|
+
const patterns = readConfigRecord(rule[1]).patterns
|
|
2403
|
+
if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
|
|
2404
|
+
for (const [patternIndex, patternValue] of patterns.entries()) {
|
|
2405
|
+
const pattern = readConfigRecord(patternValue)
|
|
2406
|
+
const message = pattern.message
|
|
2407
|
+
if (typeof message !== 'string') throw new Error('Missing pattern message')
|
|
2408
|
+
const sources: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
|
|
2409
|
+
? [
|
|
2410
|
+
['https://host/x', true],
|
|
2411
|
+
['data:text/plain,x', true],
|
|
2412
|
+
['node:fs', false],
|
|
2413
|
+
['n:entry', true],
|
|
2414
|
+
['no:entry', true],
|
|
2415
|
+
['nod:entry', true],
|
|
2416
|
+
['nodea:entry', true],
|
|
2417
|
+
['1bad:entry', false],
|
|
2418
|
+
['./data:entry', false],
|
|
2419
|
+
]
|
|
2420
|
+
: message.includes('noncanonical dot')
|
|
2421
|
+
? [
|
|
2422
|
+
['../core/../server/index.js', true],
|
|
2423
|
+
['../core/./index.js', true],
|
|
2424
|
+
['../core/index.js', false],
|
|
2425
|
+
['./index.js', false],
|
|
2426
|
+
['../../core/index.js', false],
|
|
2427
|
+
['.../../index.js', true],
|
|
2428
|
+
['.name/../index.js', true],
|
|
2429
|
+
]
|
|
2430
|
+
: message.includes('sibling sheet')
|
|
2431
|
+
? [
|
|
2432
|
+
['@src/print', true],
|
|
2433
|
+
['../print/index.js', true],
|
|
2434
|
+
['@src/core', false],
|
|
2435
|
+
['@src/browser', false],
|
|
2436
|
+
['@src/vue', false],
|
|
2437
|
+
['@app/core', false],
|
|
2438
|
+
['@app/browser', false],
|
|
2439
|
+
['@src/corex', true],
|
|
2440
|
+
['@src/', true],
|
|
2441
|
+
['../src/print/index.js', true],
|
|
2442
|
+
['../browser?x', true],
|
|
2443
|
+
['../print.ext/index.js', false],
|
|
2444
|
+
...(Array.isArray(block.files) &&
|
|
2445
|
+
block.files.some(
|
|
2446
|
+
(file: unknown) => typeof file === 'string' && file.startsWith('app/vue/'),
|
|
2447
|
+
)
|
|
2448
|
+
? [['@app/print', true] as const]
|
|
2449
|
+
: []),
|
|
2450
|
+
]
|
|
2451
|
+
: [
|
|
2452
|
+
[
|
|
2453
|
+
message.includes('absolute paths')
|
|
2454
|
+
? '/machine/module.js'
|
|
2455
|
+
: message.includes('normalized traversal')
|
|
2456
|
+
? './../core/index.js'
|
|
2457
|
+
: message.includes('forward slashes')
|
|
2458
|
+
? '..\\core\\index.js'
|
|
2459
|
+
: message.includes('compiler API')
|
|
2460
|
+
? 'typescript'
|
|
2461
|
+
: message.includes('private app')
|
|
2462
|
+
? '@app/core'
|
|
2463
|
+
: message.includes('stylesheets')
|
|
2464
|
+
? './index.css'
|
|
2465
|
+
: message.includes('host-independent') ||
|
|
2466
|
+
message.includes('Node or server')
|
|
2467
|
+
? 'node:fs'
|
|
2468
|
+
: message.includes('Vue extension')
|
|
2469
|
+
? '@src/vue'
|
|
2470
|
+
: '@src/browser',
|
|
2471
|
+
true,
|
|
2472
|
+
],
|
|
2473
|
+
]
|
|
2474
|
+
const owner =
|
|
2475
|
+
Array.isArray(block.files) && typeof block.files[0] === 'string'
|
|
2476
|
+
? block.files[0].split('/**')[0]
|
|
2477
|
+
: undefined
|
|
2478
|
+
if (owner === undefined) throw new Error('Missing pattern owner')
|
|
2479
|
+
const paths: string[] = []
|
|
2480
|
+
for (const [index, [source, refusal]] of [
|
|
2481
|
+
...sources,
|
|
2482
|
+
['./index.js', false] as const,
|
|
2483
|
+
].entries()) {
|
|
2484
|
+
const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
|
|
2485
|
+
mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
|
|
2486
|
+
writeFileSync(
|
|
2487
|
+
resolve(scratch, path),
|
|
2488
|
+
'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
|
|
2489
|
+
)
|
|
2490
|
+
expected.set(path, refusal)
|
|
2491
|
+
paths.push(path)
|
|
2492
|
+
}
|
|
2493
|
+
overrides.push({
|
|
2494
|
+
files: paths,
|
|
2495
|
+
rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
|
|
2496
|
+
})
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
writeFileSync(
|
|
2500
|
+
resolve(scratch, '.oxlintrc.json'),
|
|
2501
|
+
JSON.stringify({ rules: { 'no-debugger': 'error' }, overrides }),
|
|
2502
|
+
)
|
|
2503
|
+
const codes = readImportDiagnostics(scratch, [...expected.keys()])
|
|
2504
|
+
const readings = [...expected].map(([path, refusal]) => ({
|
|
2505
|
+
path,
|
|
2506
|
+
expected: refusal,
|
|
2507
|
+
actual: codes.has('eslint(no-restricted-imports) ' + path),
|
|
2508
|
+
}))
|
|
2509
|
+
expect(readings.filter((reading) => reading.expected !== reading.actual)).toEqual([])
|
|
2510
|
+
writeFileSync(
|
|
2511
|
+
resolve(scratch, '.oxlintrc.json'),
|
|
2512
|
+
JSON.stringify({ rules: { 'no-debugger': 'error', 'no-restricted-imports': 'off' } }),
|
|
2513
|
+
)
|
|
2514
|
+
const disabled = readImportDiagnostics(scratch, [...expected.keys()])
|
|
2515
|
+
expect(() =>
|
|
2516
|
+
expect(
|
|
2517
|
+
[...expected].filter(
|
|
2518
|
+
([path, refusal]) => disabled.has('eslint(no-restricted-imports) ' + path) !== refusal,
|
|
2519
|
+
),
|
|
2520
|
+
).toEqual([]),
|
|
2521
|
+
).toThrow('deeply equal')
|
|
2522
|
+
} finally {
|
|
2523
|
+
fixture.destroy()
|
|
2524
|
+
}
|
|
2525
|
+
})
|
|
2526
|
+
|
|
2527
|
+
it('fences environment and root imports through the real linter and fails with the restriction disabled', () => {
|
|
2528
|
+
const fixture = createPolicyScratch({ prefix: 'propagation-imports-' })
|
|
2529
|
+
const scratch = fixture.path
|
|
2530
|
+
try {
|
|
2531
|
+
mkdirSync(resolve(scratch, 'configs'))
|
|
2532
|
+
writeFileSync(
|
|
2533
|
+
resolve(scratch, 'configs/policy.ts'),
|
|
2534
|
+
readFileSync(resolve(root, 'configs/policy.ts')),
|
|
2535
|
+
)
|
|
2536
|
+
const text = readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')
|
|
2537
|
+
writeFileSync(resolve(scratch, '.oxlintrc.json'), text)
|
|
2538
|
+
const expected = new Map<string, boolean>()
|
|
2539
|
+
for (const owner of [
|
|
2540
|
+
'src/vue',
|
|
2541
|
+
'app/vue',
|
|
2542
|
+
'src/browser',
|
|
2543
|
+
'app/browser',
|
|
2544
|
+
'src/core',
|
|
2545
|
+
'app/core',
|
|
2546
|
+
'src/server',
|
|
2547
|
+
'app/server',
|
|
2548
|
+
'src/bin',
|
|
2549
|
+
]) {
|
|
2550
|
+
const refused = owner.endsWith('/vue')
|
|
2551
|
+
? [
|
|
2552
|
+
'node:fs',
|
|
2553
|
+
'@src/server',
|
|
2554
|
+
'../server/index.js',
|
|
2555
|
+
'@src/print',
|
|
2556
|
+
'../print/index.js',
|
|
2557
|
+
...(owner.startsWith('src/')
|
|
2558
|
+
? ['@app/core', '../../app/core/index.js']
|
|
2559
|
+
: ['@app/server', '@app/print']),
|
|
2560
|
+
]
|
|
2561
|
+
: [
|
|
2562
|
+
'vue',
|
|
2563
|
+
'vue/runtime-dom',
|
|
2564
|
+
'@vue/runtime-core',
|
|
2565
|
+
'@vitejs/plugin-vue',
|
|
2566
|
+
'@src/vue',
|
|
2567
|
+
'@orkestrel/sample/vue',
|
|
2568
|
+
'@app/vue',
|
|
2569
|
+
'../vue/index.js',
|
|
2570
|
+
'../../src/vue/index.js',
|
|
2571
|
+
'../../app/vue/index.js',
|
|
2572
|
+
]
|
|
2573
|
+
const admitted = owner.endsWith('/vue')
|
|
2574
|
+
? [
|
|
2575
|
+
'@src/core',
|
|
2576
|
+
'@src/browser',
|
|
2577
|
+
'../core/index.js',
|
|
2578
|
+
'../browser/index.js',
|
|
2579
|
+
'vue',
|
|
2580
|
+
'@vue/runtime-core',
|
|
2581
|
+
...(owner.startsWith('app/')
|
|
2582
|
+
? ['@app/core', '@app/browser', '@src/vue', '../../src/vue/index.js']
|
|
2583
|
+
: []),
|
|
2584
|
+
]
|
|
2585
|
+
: ['@src/core']
|
|
2586
|
+
for (const [sources, refusal] of [
|
|
2587
|
+
[
|
|
2588
|
+
[
|
|
2589
|
+
...refused,
|
|
2590
|
+
'https://host/x',
|
|
2591
|
+
'data:text/plain,x',
|
|
2592
|
+
'../core/../server/index.js',
|
|
2593
|
+
'../core/./index.js',
|
|
2594
|
+
],
|
|
2595
|
+
true,
|
|
2596
|
+
],
|
|
2597
|
+
[
|
|
2598
|
+
[
|
|
2599
|
+
...admitted,
|
|
2600
|
+
'./index.js',
|
|
2601
|
+
'../core/index.js',
|
|
2602
|
+
...(owner.endsWith('/server') || owner === 'src/bin' ? ['node:fs'] : []),
|
|
2603
|
+
],
|
|
2604
|
+
false,
|
|
2605
|
+
],
|
|
2606
|
+
] as const) {
|
|
2607
|
+
for (const [index, source] of sources.entries()) {
|
|
2608
|
+
const file = owner + '/' + (refusal ? 'refused' : 'admitted') + index + '.ts'
|
|
2609
|
+
mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
|
|
2610
|
+
writeFileSync(
|
|
2611
|
+
resolve(scratch, file),
|
|
2612
|
+
'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
|
|
2613
|
+
)
|
|
2614
|
+
expected.set(file, refusal)
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
for (const [file, source, refusal] of [
|
|
2619
|
+
['probe.config.ts', 'typescript', true],
|
|
2620
|
+
['allowed.config.ts', 'node:fs', false],
|
|
2621
|
+
['src/core/compiler.ts', 'typescript', true],
|
|
2622
|
+
['tests/compiler.ts', 'typescript', true],
|
|
2623
|
+
['configs/compiler.ts', 'typescript', true],
|
|
2624
|
+
['scripts/compiler.ts', 'typescript', true],
|
|
2625
|
+
] as const) {
|
|
2626
|
+
mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
|
|
2627
|
+
writeFileSync(
|
|
2628
|
+
resolve(scratch, file),
|
|
2629
|
+
'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
|
|
2630
|
+
)
|
|
2631
|
+
expected.set(file, refusal)
|
|
2632
|
+
}
|
|
2633
|
+
const reports: Array<ReadonlySet<string>> = []
|
|
2634
|
+
for (const disabled of [false, true]) {
|
|
2635
|
+
if (disabled) {
|
|
2636
|
+
const parsed: unknown = JSON.parse(text)
|
|
2637
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
2638
|
+
throw new Error('Missing lint configuration')
|
|
2639
|
+
const overrides: unknown = Object.getOwnPropertyDescriptor(parsed, 'overrides')?.value
|
|
2640
|
+
if (!Array.isArray(overrides)) throw new Error('Missing lint overrides')
|
|
2641
|
+
for (const block of overrides) {
|
|
2642
|
+
if (typeof block !== 'object' || block === null) continue
|
|
2643
|
+
const files: unknown = Object.getOwnPropertyDescriptor(block, 'files')?.value
|
|
2644
|
+
if (
|
|
2645
|
+
!Array.isArray(files) ||
|
|
2646
|
+
!files.some(
|
|
2647
|
+
(file: unknown) => typeof file === 'string' && file.startsWith('src/vue/'),
|
|
2648
|
+
)
|
|
2649
|
+
)
|
|
2650
|
+
continue
|
|
2651
|
+
const rules: unknown = Object.getOwnPropertyDescriptor(block, 'rules')?.value
|
|
2652
|
+
if (typeof rules !== 'object' || rules === null) throw new Error('Missing Vue rules')
|
|
2653
|
+
Reflect.set(rules, 'no-restricted-imports', 'off')
|
|
2654
|
+
}
|
|
2655
|
+
writeFileSync(resolve(scratch, '.oxlintrc.json'), JSON.stringify(parsed))
|
|
2656
|
+
}
|
|
2657
|
+
const codes = readImportDiagnostics(scratch, [...expected.keys()])
|
|
2658
|
+
reports.push(codes)
|
|
2659
|
+
}
|
|
2660
|
+
const [enabled, disabled] = reports
|
|
2661
|
+
if (enabled === undefined || disabled === undefined)
|
|
2662
|
+
throw new Error('Missing boundary readings')
|
|
2663
|
+
const readings = [...expected].map(([path, refusal]) => ({
|
|
2664
|
+
path,
|
|
2665
|
+
expected: refusal,
|
|
2666
|
+
actual: enabled.has('eslint(no-restricted-imports) ' + path),
|
|
2667
|
+
}))
|
|
2668
|
+
expect(readings.filter((reading) => reading.actual !== reading.expected)).toEqual([])
|
|
2669
|
+
const control = [...expected].map(([path, refusal]) => ({
|
|
2670
|
+
path,
|
|
2671
|
+
expected: refusal,
|
|
2672
|
+
actual: disabled.has('eslint(no-restricted-imports) ' + path),
|
|
2673
|
+
}))
|
|
2674
|
+
expect(() =>
|
|
2675
|
+
expect(control.filter((reading) => reading.actual !== reading.expected)).toEqual([]),
|
|
2676
|
+
).toThrow('deeply equal')
|
|
2677
|
+
expect(control).toContainEqual({ path: 'src/vue/refused3.ts', expected: true, actual: false })
|
|
2678
|
+
expect(control).toContainEqual({ path: 'src/vue/refused8.ts', expected: true, actual: false })
|
|
2679
|
+
} finally {
|
|
2680
|
+
fixture.destroy()
|
|
2681
|
+
}
|
|
2682
|
+
})
|
|
2683
|
+
|
|
1877
2684
|
it('enables every plugin rule over the population its law names', () => {
|
|
1878
2685
|
const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
|
|
1879
2686
|
const rules: unknown = Object.getOwnPropertyDescriptor(policyPlugin, 'rules')?.value
|
|
@@ -2063,6 +2870,129 @@ describe('policy plugin', () => {
|
|
|
2063
2870
|
})
|
|
2064
2871
|
|
|
2065
2872
|
describe('configuration helpers', () => {
|
|
2873
|
+
it('resolves declared application modes and refuses undeclared modes', () => {
|
|
2874
|
+
const applications = { browser: true, vue: true }
|
|
2875
|
+
for (const mode of [undefined, 'development', 'production', 'test', 'browser']) {
|
|
2876
|
+
expect(configHelpers.resolveApplication(mode, applications)).toBe('browser')
|
|
2877
|
+
}
|
|
2878
|
+
expect(configHelpers.resolveApplication('vue', applications)).toBe('vue')
|
|
2879
|
+
expect(() => configHelpers.resolveApplication('vue', { browser: true })).toThrow(
|
|
2880
|
+
'The application mode "vue" is not declared.',
|
|
2881
|
+
)
|
|
2882
|
+
expect(() => configHelpers.resolveApplication('preview', applications)).toThrow(
|
|
2883
|
+
'The application mode "preview" is not declared.',
|
|
2884
|
+
)
|
|
2885
|
+
expect(() => configHelpers.resolveApplication('toString', applications)).toThrow(
|
|
2886
|
+
'The application mode "toString" is not declared.',
|
|
2887
|
+
)
|
|
2888
|
+
})
|
|
2889
|
+
|
|
2890
|
+
it('hashes page bytes against SHA-256 vectors without its stamp line', () => {
|
|
2891
|
+
expect(configHelpers.computeStamp('')).toBe(
|
|
2892
|
+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
2893
|
+
)
|
|
2894
|
+
expect(configHelpers.computeStamp('abc')).toBe(
|
|
2895
|
+
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
|
|
2896
|
+
)
|
|
2897
|
+
expect(configHelpers.computeStamp('<meta name="build-id" content="old" />\nabc')).toBe(
|
|
2898
|
+
configHelpers.computeStamp('abc'),
|
|
2899
|
+
)
|
|
2900
|
+
expect(configHelpers.computeStamp('<meta name="build-id" content="old" />\r\nabc')).toBe(
|
|
2901
|
+
configHelpers.computeStamp('abc'),
|
|
2902
|
+
)
|
|
2903
|
+
expect(configHelpers.computeStamp('abd')).not.toBe(configHelpers.computeStamp('abc'))
|
|
2904
|
+
})
|
|
2905
|
+
|
|
2906
|
+
it('stamps the final page once and refuses repeated or malformed stamp lines', () => {
|
|
2907
|
+
const html = '<html>\n\t<head>\n\t</head>\n</html>\n'
|
|
2908
|
+
const stamped = configHelpers.stampPage(html)
|
|
2909
|
+
expect(stamped.match(/name="build-id"/gu)).toHaveLength(1)
|
|
2910
|
+
expect(stamped).toContain(`content="${configHelpers.computeStamp(stamped)}"`)
|
|
2911
|
+
expect(configHelpers.computeStamp(stamped)).toBe(configHelpers.computeStamp(html))
|
|
2912
|
+
expect(configHelpers.stampPage(stamped)).toBe(stamped)
|
|
2913
|
+
const fixture = createPolicyScratch({ prefix: 'propagation-stamp-' })
|
|
2914
|
+
const scratch = fixture.path
|
|
2915
|
+
try {
|
|
2916
|
+
const path = resolve(scratch, 'page.html')
|
|
2917
|
+
writeFileSync(path, stamped)
|
|
2918
|
+
expect(configHelpers.stampPage(readFileSync(path, 'utf8'))).toBe(stamped)
|
|
2919
|
+
writeFileSync(path, '<meta name="build-id" content="old" />\n' + stamped)
|
|
2920
|
+
expect(() => configHelpers.stampPage(readFileSync(path, 'utf8'))).toThrow(
|
|
2921
|
+
'at most one well-formed build stamp line',
|
|
2922
|
+
)
|
|
2923
|
+
} finally {
|
|
2924
|
+
fixture.destroy()
|
|
2925
|
+
}
|
|
2926
|
+
expect(() =>
|
|
2927
|
+
configHelpers.stampPage('<meta name="build-id" content="old" />\n' + stamped),
|
|
2928
|
+
).toThrow('A showcase page must carry at most one well-formed build stamp line.')
|
|
2929
|
+
expect(() => configHelpers.stampPage('<html><head></head></html>')).toThrow(
|
|
2930
|
+
'A showcase page must close its head on its own line.',
|
|
2931
|
+
)
|
|
2932
|
+
expect(() => configHelpers.stampPage('<meta name="build-id" content="old">\n' + html)).toThrow(
|
|
2933
|
+
'A showcase page must carry at most one well-formed build stamp line.',
|
|
2934
|
+
)
|
|
2935
|
+
})
|
|
2936
|
+
|
|
2937
|
+
it('resolves externals from peers, refused packages, and published siblings', () => {
|
|
2938
|
+
const options = {
|
|
2939
|
+
peers: ['sample-peer'],
|
|
2940
|
+
refused: ['vue', '@vue/'],
|
|
2941
|
+
siblings: ['C:/workspace/src/browser/index.ts'],
|
|
2942
|
+
}
|
|
2943
|
+
for (const id of [
|
|
2944
|
+
'node:fs',
|
|
2945
|
+
'@orkestrel/contract',
|
|
2946
|
+
'sample-peer',
|
|
2947
|
+
'sample-peer/subpath',
|
|
2948
|
+
'C:\\workspace\\src\\browser\\index.ts',
|
|
2949
|
+
]) {
|
|
2950
|
+
expect(configHelpers.resolveExternal(id, options)).toBe(true)
|
|
2951
|
+
}
|
|
2952
|
+
for (const id of [
|
|
2953
|
+
'@src/browser',
|
|
2954
|
+
'@src/core',
|
|
2955
|
+
'@app/vue',
|
|
2956
|
+
'sample-peerish',
|
|
2957
|
+
'./local.js',
|
|
2958
|
+
'vue-tools',
|
|
2959
|
+
]) {
|
|
2960
|
+
expect(configHelpers.resolveExternal(id, options)).toBe(false)
|
|
2961
|
+
}
|
|
2962
|
+
for (const id of ['vue', 'vue/runtime-dom']) {
|
|
2963
|
+
expect(() => configHelpers.resolveExternal(id, options)).toThrow(
|
|
2964
|
+
`The import ${id} is refused`,
|
|
2965
|
+
)
|
|
2966
|
+
expect(() => configHelpers.resolveExternal(id, options)).toThrow('peerDependencies')
|
|
2967
|
+
}
|
|
2968
|
+
expect(configHelpers.resolveExternal('vue', { ...options, peers: ['vue'] })).toBe(true)
|
|
2969
|
+
expect(configHelpers.resolveExternal('vue/runtime-dom', { ...options, peers: ['vue'] })).toBe(
|
|
2970
|
+
true,
|
|
2971
|
+
)
|
|
2972
|
+
expect(() =>
|
|
2973
|
+
configHelpers.resolveExternal('@vue/runtime-core', {
|
|
2974
|
+
...options,
|
|
2975
|
+
peers: ['vue', '@vue/runtime-core'],
|
|
2976
|
+
}),
|
|
2977
|
+
).toThrow('import from vue instead of the @vue/ implementation scope')
|
|
2978
|
+
})
|
|
2979
|
+
|
|
2980
|
+
it('classifies Vue faces as browser owners and browser targets', () => {
|
|
2981
|
+
for (const owner of ['src/vue', 'app/vue']) {
|
|
2982
|
+
expect(configHelpers.environmentPathError(owner, 'src/server/index.ts')).toBeDefined()
|
|
2983
|
+
expect(configHelpers.environmentSourceError(owner, 'node:fs')).toBeDefined()
|
|
2984
|
+
expect(configHelpers.environmentSourceError(owner, '@src/server')).toBeDefined()
|
|
2985
|
+
expect(configHelpers.environmentPathError(owner, 'src/browser/index.ts')).toBeUndefined()
|
|
2986
|
+
expect(configHelpers.environmentSourceError(owner, '@src/browser')).toBeUndefined()
|
|
2987
|
+
}
|
|
2988
|
+
for (const owner of ['src/core', 'app/core', 'src/server', 'app/server']) {
|
|
2989
|
+
expect(configHelpers.environmentPathError(owner, 'src/vue/index.ts')).toBeDefined()
|
|
2990
|
+
expect(configHelpers.environmentPathError(owner, 'app/vue/index.ts')).toBeDefined()
|
|
2991
|
+
expect(configHelpers.environmentSourceError(owner, '@src/vue')).toBeDefined()
|
|
2992
|
+
expect(configHelpers.environmentSourceError(owner, '@app/vue')).toBeDefined()
|
|
2993
|
+
}
|
|
2994
|
+
})
|
|
2995
|
+
|
|
2066
2996
|
it('exposes every helper this proof requires', () => {
|
|
2067
2997
|
const required = [
|
|
2068
2998
|
'ENVIRONMENT_MODULE_BYTES',
|
|
@@ -2250,7 +3180,8 @@ describe('configuration helpers', () => {
|
|
|
2250
3180
|
> = ['src/core', 'src/browser', 'src/server', 'app/core', 'app/browser', 'app/server']
|
|
2251
3181
|
const owner = environments.find((environment) => existsSync(resolve(root, environment)))
|
|
2252
3182
|
if (owner === undefined) throw new Error('The workspace carries no environment plugin target')
|
|
2253
|
-
const
|
|
3183
|
+
const scratch = createPolicyScratch({ parent: resolve(root, owner), prefix: 'config-build-' })
|
|
3184
|
+
const workspace = scratch.path
|
|
2254
3185
|
try {
|
|
2255
3186
|
const source = resolve(workspace, 'index.ts')
|
|
2256
3187
|
writeFileSync(source, 'export const control = true\n', 'utf8')
|
|
@@ -2291,7 +3222,7 @@ describe('configuration helpers', () => {
|
|
|
2291
3222
|
)
|
|
2292
3223
|
await expect(Reflect.apply(hook, context, ['@src/core', source])).resolves.toBeNull()
|
|
2293
3224
|
} finally {
|
|
2294
|
-
|
|
3225
|
+
scratch.destroy()
|
|
2295
3226
|
}
|
|
2296
3227
|
})
|
|
2297
3228
|
|
|
@@ -2413,6 +3344,16 @@ describe('configuration helpers', () => {
|
|
|
2413
3344
|
expect(configHelpers.rewriteCoreSpecifier("from '@src/core'")).toBe(`from '${name}'`)
|
|
2414
3345
|
expect(configHelpers.rewriteCoreSpecifier("from '../../core/index.js'")).toBe(`from '${name}'`)
|
|
2415
3346
|
expect(configHelpers.rewriteCoreSpecifier("from './sibling.js'")).toBe("from './sibling.js'")
|
|
3347
|
+
expect(configHelpers.rewriteBrowserSpecifier("from '@src/browser'")).toBe(
|
|
3348
|
+
`from '${name}/browser'`,
|
|
3349
|
+
)
|
|
3350
|
+
expect(configHelpers.rewriteBrowserSpecifier("from '../../browser/index.js'")).toBe(
|
|
3351
|
+
`from '${name}/browser'`,
|
|
3352
|
+
)
|
|
3353
|
+
expect(configHelpers.rewriteBrowserSpecifier("from '../browser/index.ts'")).toBe(
|
|
3354
|
+
`from '${name}/browser'`,
|
|
3355
|
+
)
|
|
3356
|
+
expect(configHelpers.rewriteBrowserSpecifier("from './sibling.js'")).toBe("from './sibling.js'")
|
|
2416
3357
|
|
|
2417
3358
|
// The mechanism the roll-up proof's skip reads: an absent package rejects resolution.
|
|
2418
3359
|
expect(() => createRequire(import.meta.url).resolve('@absent/declaration-extractor')).toThrow(
|
|
@@ -2459,7 +3400,7 @@ describe('configuration helpers', () => {
|
|
|
2459
3400
|
// The roll-up loads the declaration extractor, which only a workspace publishing source from
|
|
2460
3401
|
// `src` installs. Where `require.resolve` rejects that package, this proof does not apply.
|
|
2461
3402
|
it.skipIf(extractorPath === undefined)(
|
|
2462
|
-
'rolls one face into a single declaration and rewrites its core
|
|
3403
|
+
'rolls one face into a single declaration and rewrites its core and browser specifiers',
|
|
2463
3404
|
async () => {
|
|
2464
3405
|
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-rollup-' })
|
|
2465
3406
|
// The hook's temporary declaration emit is proven removed from a host temporary
|
|
@@ -2494,7 +3435,10 @@ describe('configuration helpers', () => {
|
|
|
2494
3435
|
noEmit: false,
|
|
2495
3436
|
rootDir: './source',
|
|
2496
3437
|
outDir: './emit',
|
|
2497
|
-
paths: {
|
|
3438
|
+
paths: {
|
|
3439
|
+
'@src/core': ['./source/core/index.ts'],
|
|
3440
|
+
'@src/browser': ['./source/browser/index.ts'],
|
|
3441
|
+
},
|
|
2498
3442
|
},
|
|
2499
3443
|
include: ['./source/server/**/*.ts'],
|
|
2500
3444
|
}),
|
|
@@ -2503,9 +3447,13 @@ describe('configuration helpers', () => {
|
|
|
2503
3447
|
'source/core/index.ts',
|
|
2504
3448
|
'export interface FixtureLabel {\n\treadonly label: string\n}\n',
|
|
2505
3449
|
)
|
|
3450
|
+
scratch.write(
|
|
3451
|
+
'source/browser/index.ts',
|
|
3452
|
+
'export interface FixtureView {\n\treadonly visible: boolean\n}\n',
|
|
3453
|
+
)
|
|
2506
3454
|
scratch.write(
|
|
2507
3455
|
'source/server/index.ts',
|
|
2508
|
-
"import type { FixtureLabel } from '@src/core'\n\nexport interface FixtureRecord {\n\treadonly label: FixtureLabel\n\treadonly count: number\n}\n",
|
|
3456
|
+
"import type { FixtureLabel } from '@src/core'\nimport type { FixtureView } from '@src/browser'\n\nexport interface FixtureRecord {\n\treadonly label: FixtureLabel\n\treadonly view: FixtureView\n\treadonly count: number\n}\n",
|
|
2509
3457
|
)
|
|
2510
3458
|
|
|
2511
3459
|
const name = configHelpers.packageManifestName(configHelpers.WORKSPACE_ROOT)
|
|
@@ -2525,7 +3473,13 @@ describe('configuration helpers', () => {
|
|
|
2525
3473
|
plugins: [
|
|
2526
3474
|
configHelpers.declarationRollup(
|
|
2527
3475
|
face.rewrite
|
|
2528
|
-
? {
|
|
3476
|
+
? {
|
|
3477
|
+
project,
|
|
3478
|
+
rewrite: (text) =>
|
|
3479
|
+
configHelpers.rewriteBrowserSpecifier(
|
|
3480
|
+
configHelpers.rewriteCoreSpecifier(text),
|
|
3481
|
+
),
|
|
3482
|
+
}
|
|
2529
3483
|
: { project },
|
|
2530
3484
|
),
|
|
2531
3485
|
],
|
|
@@ -2572,10 +3526,13 @@ describe('configuration helpers', () => {
|
|
|
2572
3526
|
expect(rolled).toContain('FixtureRecord')
|
|
2573
3527
|
expect(rolled).toContain(`from '${name}'`)
|
|
2574
3528
|
expect(rolled).not.toContain('@src/core')
|
|
3529
|
+
expect(rolled).toContain(`from '${name}/browser'`)
|
|
3530
|
+
expect(rolled).not.toContain('@src/browser')
|
|
2575
3531
|
|
|
2576
3532
|
// The control: the same face without a rewrite ships the specifier the extractor kept.
|
|
2577
3533
|
const control = readFileSync(join(workspace, 'kept', 'index.d.ts'), 'utf8')
|
|
2578
3534
|
expect(control).toContain('@src/core')
|
|
3535
|
+
expect(control).toContain('@src/browser')
|
|
2579
3536
|
expect(control).not.toContain(`from '${name}'`)
|
|
2580
3537
|
} finally {
|
|
2581
3538
|
for (const [name, value] of inherited) {
|