@orkestrel/scaffold 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -99
- package/dist/bin/main.js +1094 -0
- package/dist/bin/main.js.map +1 -0
- package/dist/host/CLAUDE.md +3 -1
- package/dist/host/agents/orchestration.md +61 -4
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +7 -5
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +9 -9
- package/dist/host/claude/rules/architecture.md +45 -3
- package/dist/host/claude/rules/quality.md +4 -0
- package/dist/host/claude/rules/tests.md +57 -1
- package/dist/host/claude/rules/workspace.md +50 -17
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/configs/helpers.ts +762 -0
- package/dist/host/dotfiles/oxlintrc.json +2 -1
- package/dist/host/guides/scaffold.md +862 -0
- package/dist/host/manifest.json +40 -33
- package/dist/host/tests/config.test.ts +544 -0
- package/dist/host/tests/policy.test.ts +46 -0
- package/dist/host/tests/setupPolicy.ts +557 -602
- package/dist/src/core/index.cjs +3569 -10510
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +2361 -2789
- package/dist/src/core/index.d.ts +2361 -2789
- package/dist/src/core/index.js +3513 -10374
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +2855 -3765
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1920 -1335
- package/dist/src/server/index.d.ts +1920 -1335
- package/dist/src/server/index.js +2812 -3680
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -23
- package/dist/bin/scaffold.js +0 -1896
- package/dist/bin/scaffold.js.map +0 -1
- package/dist/host/guides/src/scaffold.md +0 -2886
- /package/dist/host/guides/{src/guide.md → guide.md} +0 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import type { Plugin } from 'vite'
|
|
2
|
+
import { parseSync, transformWithOxc, Visitor } from 'vite'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { isBuiltin } from 'node:module'
|
|
5
|
+
import {
|
|
6
|
+
closeSync,
|
|
7
|
+
constants as FS_CONSTANTS,
|
|
8
|
+
existsSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
} from 'node:fs'
|
|
15
|
+
import { dirname, isAbsolute, relative, resolve as resolvePath, sep } from 'node:path'
|
|
16
|
+
|
|
17
|
+
export function hasAsciiUrlControl(value: string): boolean {
|
|
18
|
+
for (const character of value) {
|
|
19
|
+
const code = character.codePointAt(0)
|
|
20
|
+
if (code !== undefined && (code <= 0x1f || code === 0x7f)) return true
|
|
21
|
+
}
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const PACKAGE_MANIFEST_BYTES = 1_048_576
|
|
26
|
+
export const ENVIRONMENT_MODULE_BYTES = 8_388_608
|
|
27
|
+
|
|
28
|
+
export const WORKSPACE_ROOT = realpathSync.native(
|
|
29
|
+
resolvePath(dirname(fileURLToPath(import.meta.url)), '..'),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export function fileSystemPath(pathname: string): string {
|
|
33
|
+
if (!pathname.startsWith('/@fs/')) return pathname
|
|
34
|
+
const candidate = pathname.slice('/@fs/'.length)
|
|
35
|
+
// Vite URL normalization can collapse the leading slash of a POSIX absolute path.
|
|
36
|
+
return candidate.startsWith('/') || /^[A-Za-z]:[\\/]/.test(candidate)
|
|
37
|
+
? candidate
|
|
38
|
+
: `/${candidate}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function physicalPath(path: string): string {
|
|
42
|
+
const [pathWithoutQuery] = path.split('?')
|
|
43
|
+
const candidate = fileSystemPath(pathWithoutQuery ?? path)
|
|
44
|
+
const physicalCandidate = /^file:/i.test(candidate) ? fileURLToPath(candidate) : candidate
|
|
45
|
+
const absoluteCandidate =
|
|
46
|
+
physicalCandidate.length === 0
|
|
47
|
+
? WORKSPACE_ROOT
|
|
48
|
+
: isAbsolute(physicalCandidate)
|
|
49
|
+
? physicalCandidate
|
|
50
|
+
: resolvePath(WORKSPACE_ROOT, physicalCandidate)
|
|
51
|
+
return existsSync(absoluteCandidate) ? realpathSync.native(absoluteCandidate) : absoluteCandidate
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function sourceFallback(importer: string, source: string): string {
|
|
55
|
+
return /^file:/i.test(source) ? fileURLToPath(source) : resolvePath(dirname(importer), source)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function workspacePath(path: string): string | undefined {
|
|
59
|
+
const relativePath = relative(WORKSPACE_ROOT, physicalPath(path)).replaceAll('\\', '/')
|
|
60
|
+
if (relativePath === '..' || relativePath.startsWith('../') || isAbsolute(relativePath)) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
return relativePath
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isBoundaryExemptModule(id: string): boolean {
|
|
67
|
+
const normalizedId = id.replaceAll('\\', '/')
|
|
68
|
+
const [path] = normalizedId.split(/[?#]/)
|
|
69
|
+
if (
|
|
70
|
+
path === undefined ||
|
|
71
|
+
normalizedId.startsWith('\0') ||
|
|
72
|
+
normalizedId.includes('virtual:') ||
|
|
73
|
+
normalizedId === '@vite/client' ||
|
|
74
|
+
normalizedId === '@vite/env' ||
|
|
75
|
+
normalizedId.startsWith('/@id/') ||
|
|
76
|
+
normalizedId.startsWith('/@vite/') ||
|
|
77
|
+
normalizedId.startsWith('/__vite') ||
|
|
78
|
+
normalizedId.startsWith('/__vitest') ||
|
|
79
|
+
normalizedId.startsWith('@vitest/browser') ||
|
|
80
|
+
normalizedId.includes('/@vitest/browser/')
|
|
81
|
+
) {
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
let physicalId: string | undefined
|
|
85
|
+
try {
|
|
86
|
+
physicalId = physicalPath(id).replaceAll('\\', '/')
|
|
87
|
+
} catch {
|
|
88
|
+
physicalId = undefined
|
|
89
|
+
}
|
|
90
|
+
for (const candidate of physicalId === undefined ? [path] : [path, physicalId]) {
|
|
91
|
+
if (candidate.split('/').some((segment) => segment.toLowerCase() === 'node_modules')) {
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isWorkspaceBoundaryModule(id: string): boolean {
|
|
99
|
+
if (isBoundaryExemptModule(id)) return false
|
|
100
|
+
const normalizedId = id.replaceAll('\\', '/')
|
|
101
|
+
const [path] = normalizedId.split(/[?#]/)
|
|
102
|
+
if (path === undefined) return false
|
|
103
|
+
let candidate = fileSystemPath(path)
|
|
104
|
+
try {
|
|
105
|
+
if (/^file:/i.test(candidate)) candidate = fileURLToPath(candidate)
|
|
106
|
+
} catch {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
const rootRelative = /^\/(?:app|src)\/(?:core|browser|server)\//.test(candidate)
|
|
110
|
+
const absoluteCandidate = rootRelative
|
|
111
|
+
? resolvePath(WORKSPACE_ROOT, candidate.slice(1))
|
|
112
|
+
: isAbsolute(candidate)
|
|
113
|
+
? candidate
|
|
114
|
+
: resolvePath(WORKSPACE_ROOT, candidate)
|
|
115
|
+
const relativeId = relative(WORKSPACE_ROOT, absoluteCandidate).replaceAll('\\', '/')
|
|
116
|
+
return (
|
|
117
|
+
relativeId !== '..' &&
|
|
118
|
+
!relativeId.startsWith('../') &&
|
|
119
|
+
!isAbsolute(relativeId) &&
|
|
120
|
+
/^(?:app|src)\/(?:core|browser|server)\//.test(relativeId)
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isOutsideWorkspacePath(path: string): boolean {
|
|
125
|
+
const [pathWithoutQuery] = path.split('?')
|
|
126
|
+
if (pathWithoutQuery === undefined) return false
|
|
127
|
+
return isAbsolute(fileSystemPath(pathWithoutQuery))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function containedPath(root: string, target: string): boolean {
|
|
131
|
+
const relativePath = relative(root, target)
|
|
132
|
+
return (
|
|
133
|
+
relativePath === '' ||
|
|
134
|
+
(relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath))
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function packageNameOf(source: string): string | undefined {
|
|
139
|
+
const [sourcePath] = source.replaceAll('\\', '/').split(/[?#]/)
|
|
140
|
+
if (
|
|
141
|
+
sourcePath === undefined ||
|
|
142
|
+
sourcePath.length === 0 ||
|
|
143
|
+
sourcePath.startsWith('.') ||
|
|
144
|
+
sourcePath.startsWith('/') ||
|
|
145
|
+
sourcePath.startsWith('#') ||
|
|
146
|
+
sourcePath.startsWith('file:') ||
|
|
147
|
+
/^[A-Za-z]:\//.test(sourcePath) ||
|
|
148
|
+
isBuiltin(sourcePath)
|
|
149
|
+
) {
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
const segments = sourcePath.split('/')
|
|
153
|
+
if (sourcePath.startsWith('@')) {
|
|
154
|
+
const [scope, name] = segments
|
|
155
|
+
return scope === undefined || name === undefined ? undefined : `${scope}/${name}`
|
|
156
|
+
}
|
|
157
|
+
return segments[0]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readBoundedFile(path: string, limit: number): string | undefined {
|
|
161
|
+
if (!existsSync(path)) return undefined
|
|
162
|
+
try {
|
|
163
|
+
const status = lstatSync(path)
|
|
164
|
+
if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1 || status.size > limit) {
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
167
|
+
const handle = openSync(path, FS_CONSTANTS.O_RDONLY | FS_CONSTANTS.O_NOFOLLOW)
|
|
168
|
+
try {
|
|
169
|
+
const current = fstatSync(handle)
|
|
170
|
+
if (
|
|
171
|
+
!current.isFile() ||
|
|
172
|
+
current.nlink !== 1 ||
|
|
173
|
+
current.dev !== status.dev ||
|
|
174
|
+
current.ino !== status.ino ||
|
|
175
|
+
current.size !== status.size ||
|
|
176
|
+
current.mtimeMs !== status.mtimeMs ||
|
|
177
|
+
current.ctimeMs !== status.ctimeMs
|
|
178
|
+
) {
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
const bytes = Buffer.allocUnsafe(current.size + 1)
|
|
182
|
+
let offset = 0
|
|
183
|
+
for (;;) {
|
|
184
|
+
const count = readSync(handle, bytes, offset, bytes.length - offset, null)
|
|
185
|
+
if (count === 0) break
|
|
186
|
+
offset += count
|
|
187
|
+
if (offset > current.size) return undefined
|
|
188
|
+
}
|
|
189
|
+
const final = fstatSync(handle)
|
|
190
|
+
if (
|
|
191
|
+
!final.isFile() ||
|
|
192
|
+
final.nlink !== 1 ||
|
|
193
|
+
final.dev !== current.dev ||
|
|
194
|
+
final.ino !== current.ino ||
|
|
195
|
+
final.size !== current.size ||
|
|
196
|
+
final.size !== offset ||
|
|
197
|
+
final.mtimeMs !== current.mtimeMs ||
|
|
198
|
+
final.ctimeMs !== current.ctimeMs
|
|
199
|
+
) {
|
|
200
|
+
return undefined
|
|
201
|
+
}
|
|
202
|
+
return bytes.toString('utf8', 0, offset)
|
|
203
|
+
} finally {
|
|
204
|
+
closeSync(handle)
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
return undefined
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function packageManifestName(directory: string): string | undefined {
|
|
212
|
+
const content = readBoundedFile(resolvePath(directory, 'package.json'), PACKAGE_MANIFEST_BYTES)
|
|
213
|
+
if (content === undefined) return undefined
|
|
214
|
+
try {
|
|
215
|
+
const manifest: unknown = JSON.parse(content)
|
|
216
|
+
if (typeof manifest !== 'object' || manifest === null) return undefined
|
|
217
|
+
const manifestName = Object.getOwnPropertyDescriptor(manifest, 'name')?.value
|
|
218
|
+
return typeof manifestName === 'string' && packageNameOf(manifestName) === manifestName
|
|
219
|
+
? manifestName
|
|
220
|
+
: undefined
|
|
221
|
+
} catch {
|
|
222
|
+
return undefined
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function isPackageBoundary(directory: string): boolean {
|
|
227
|
+
const segments = directory.replaceAll('\\', '/').split('/')
|
|
228
|
+
let nodeModules = -1
|
|
229
|
+
for (const [index, segment] of segments.entries()) {
|
|
230
|
+
if (segment.toLowerCase() === 'node_modules') nodeModules = index
|
|
231
|
+
}
|
|
232
|
+
if (nodeModules < 0) return false
|
|
233
|
+
const packageSegments = segments.slice(nodeModules + 1)
|
|
234
|
+
return (
|
|
235
|
+
(packageSegments.length === 1 && packageSegments[0]?.startsWith('@') === false) ||
|
|
236
|
+
(packageSegments.length === 2 &&
|
|
237
|
+
packageSegments[0]?.startsWith('@') === true &&
|
|
238
|
+
packageSegments[1]?.length !== 0)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function packageRootOf(packageName: string, resolvedPath: string): string | undefined {
|
|
243
|
+
const physical = physicalPath(resolvedPath)
|
|
244
|
+
let current =
|
|
245
|
+
existsSync(physical) && lstatSync(physical).isDirectory() ? physical : dirname(physical)
|
|
246
|
+
for (;;) {
|
|
247
|
+
const boundary = isPackageBoundary(current)
|
|
248
|
+
const manifest = resolvePath(current, 'package.json')
|
|
249
|
+
if (boundary || existsSync(manifest)) {
|
|
250
|
+
return packageManifestName(current) === packageName ? realpathSync.native(current) : undefined
|
|
251
|
+
}
|
|
252
|
+
const parent = dirname(current)
|
|
253
|
+
if (parent === current) return undefined
|
|
254
|
+
current = parent
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function packageRootForResolved(resolvedPath: string): string | undefined {
|
|
259
|
+
const physical = physicalPath(resolvedPath)
|
|
260
|
+
let current =
|
|
261
|
+
existsSync(physical) && lstatSync(physical).isDirectory() ? physical : dirname(physical)
|
|
262
|
+
for (;;) {
|
|
263
|
+
const boundary = isPackageBoundary(current)
|
|
264
|
+
const manifest = resolvePath(current, 'package.json')
|
|
265
|
+
if (boundary || existsSync(manifest)) {
|
|
266
|
+
return packageManifestName(current) === undefined ? undefined : realpathSync.native(current)
|
|
267
|
+
}
|
|
268
|
+
const parent = dirname(current)
|
|
269
|
+
if (parent === current) return undefined
|
|
270
|
+
current = parent
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function trustedPackageRootFor(
|
|
275
|
+
target: string,
|
|
276
|
+
trustedPackageRoots: ReadonlySet<string>,
|
|
277
|
+
): string | undefined {
|
|
278
|
+
const physical = physicalPath(target)
|
|
279
|
+
for (const root of trustedPackageRoots) {
|
|
280
|
+
if (containedPath(root, physical)) return root
|
|
281
|
+
}
|
|
282
|
+
return undefined
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function isStylesheetPath(path: string): boolean {
|
|
286
|
+
return /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:[?#]|$)/.test(path)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function environmentPathError(owner: string, target: string): string | undefined {
|
|
290
|
+
const targetApplication = target.startsWith('app/')
|
|
291
|
+
const targetBrowser = target.startsWith('app/browser/') || target.startsWith('src/browser/')
|
|
292
|
+
const targetServer = target.startsWith('app/server/') || target.startsWith('src/server/')
|
|
293
|
+
const stylesheet = isStylesheetPath(target)
|
|
294
|
+
if (owner.startsWith('src/') && targetApplication) {
|
|
295
|
+
return 'Published modules cannot depend on private application modules'
|
|
296
|
+
}
|
|
297
|
+
if (owner.endsWith('/core') && (stylesheet || targetBrowser || targetServer)) {
|
|
298
|
+
return 'Core modules must remain host-independent'
|
|
299
|
+
}
|
|
300
|
+
if (owner.endsWith('/browser') && targetServer) {
|
|
301
|
+
return 'Browser modules cannot depend on Node or server-only modules'
|
|
302
|
+
}
|
|
303
|
+
if (owner.endsWith('/server') && (stylesheet || targetBrowser)) {
|
|
304
|
+
return 'Server modules cannot depend on Vue or browser-only modules'
|
|
305
|
+
}
|
|
306
|
+
return undefined
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function environmentSourceError(owner: string, source: string): string | undefined {
|
|
310
|
+
const normalizedSource = source.replaceAll('\\', '/')
|
|
311
|
+
if (hasAsciiUrlControl(normalizedSource)) {
|
|
312
|
+
return 'Environment module URLs cannot contain ASCII controls'
|
|
313
|
+
}
|
|
314
|
+
const [sourcePath] = normalizedSource.split(/[?#]/)
|
|
315
|
+
const builtin = sourcePath !== undefined && isBuiltin(sourcePath)
|
|
316
|
+
const unsupportedScheme =
|
|
317
|
+
sourcePath !== undefined &&
|
|
318
|
+
/^[A-Za-z][A-Za-z0-9+.-]*:/.test(sourcePath) &&
|
|
319
|
+
!builtin &&
|
|
320
|
+
!/^file:/i.test(sourcePath) &&
|
|
321
|
+
!/^[A-Za-z]:\//.test(sourcePath)
|
|
322
|
+
const browserPackage =
|
|
323
|
+
/^(?:(?:vue|vite)(?:[/?#]|$)|@(?:vue|vitejs)\/|@(?:app|src)\/browser(?:[/?#]|$)|@orkestrel\/[^/]+\/browser(?:[/?#]|$))/.test(
|
|
324
|
+
normalizedSource,
|
|
325
|
+
)
|
|
326
|
+
const serverPackage =
|
|
327
|
+
/^(?:@(?:app|src)\/server(?:[/?#]|$)|@orkestrel\/[^/]+\/server(?:[/?#]|$))/.test(
|
|
328
|
+
normalizedSource,
|
|
329
|
+
)
|
|
330
|
+
const stylesheet = isStylesheetPath(normalizedSource)
|
|
331
|
+
if (unsupportedScheme) return 'Environment modules cannot import non-Node URL schemes'
|
|
332
|
+
if (owner.startsWith('src/') && /^@app(?:[/?#]|$)/.test(normalizedSource)) {
|
|
333
|
+
return 'Published modules cannot depend on private application modules'
|
|
334
|
+
}
|
|
335
|
+
if (owner.endsWith('/core') && (builtin || browserPackage || serverPackage || stylesheet)) {
|
|
336
|
+
return 'Core modules must remain host-independent'
|
|
337
|
+
}
|
|
338
|
+
if (owner.endsWith('/browser') && (builtin || serverPackage)) {
|
|
339
|
+
return 'Browser modules cannot depend on Node or server-only modules'
|
|
340
|
+
}
|
|
341
|
+
if (owner.endsWith('/server') && (browserPackage || stylesheet)) {
|
|
342
|
+
return 'Server modules cannot depend on Vue or browser-only modules'
|
|
343
|
+
}
|
|
344
|
+
return undefined
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function enforceOutputPath(configured: string, expected: string): void {
|
|
348
|
+
if (relative(expected, configured) !== '') {
|
|
349
|
+
throw new Error(
|
|
350
|
+
'[orkestrel-output-boundary] Build output must use its exact configured workspace directory',
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
const workspaceRelative = relative(WORKSPACE_ROOT, expected)
|
|
354
|
+
if (
|
|
355
|
+
workspaceRelative === '..' ||
|
|
356
|
+
workspaceRelative.startsWith(`..${sep}`) ||
|
|
357
|
+
isAbsolute(workspaceRelative)
|
|
358
|
+
) {
|
|
359
|
+
throw new Error('[orkestrel-output-boundary] Build output must remain inside the workspace')
|
|
360
|
+
}
|
|
361
|
+
let current = WORKSPACE_ROOT
|
|
362
|
+
for (const segment of workspaceRelative.split(sep)) {
|
|
363
|
+
if (segment.length === 0) continue
|
|
364
|
+
current = resolvePath(current, segment)
|
|
365
|
+
if (!existsSync(current)) continue
|
|
366
|
+
const status = lstatSync(current)
|
|
367
|
+
if (status.isSymbolicLink() || !status.isDirectory()) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
'[orkestrel-output-boundary] Build output and its existing parents must be real directories',
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
if (workspacePath(realpathSync.native(current)) === undefined) {
|
|
373
|
+
throw new Error('[orkestrel-output-boundary] Build output must remain inside the workspace')
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function outputBoundary(output: string): Plugin {
|
|
379
|
+
const expected = resolvePath(WORKSPACE_ROOT, output)
|
|
380
|
+
let configured = expected
|
|
381
|
+
let build = false
|
|
382
|
+
return {
|
|
383
|
+
name: 'orkestrel-output-boundary',
|
|
384
|
+
enforce: 'pre',
|
|
385
|
+
configResolved(config) {
|
|
386
|
+
if (config.publicDir !== '') {
|
|
387
|
+
throw new Error(
|
|
388
|
+
'[orkestrel-output-boundary] Public directories are disabled; every output must come from the audited graph',
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
if (
|
|
392
|
+
output.endsWith('/browser') &&
|
|
393
|
+
config.build.lib === false &&
|
|
394
|
+
config.build.assetsInlineLimit !== 0
|
|
395
|
+
) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
'[orkestrel-output-boundary] Browser assets must remain external for output auditing',
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
const outputOptions = config.build.rolldownOptions.output
|
|
401
|
+
const outputs = Array.isArray(outputOptions) ? outputOptions : [outputOptions]
|
|
402
|
+
for (const options of outputs) {
|
|
403
|
+
if (options?.dir !== undefined || options?.file !== undefined) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
'[orkestrel-output-boundary] Rolldown output directories and files cannot override the configured output',
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
build = config.command === 'build'
|
|
410
|
+
configured = resolvePath(config.root, config.build.outDir)
|
|
411
|
+
},
|
|
412
|
+
buildStart() {
|
|
413
|
+
if (build) enforceOutputPath(configured, expected)
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function decodeAssetSource(source: string): string | undefined {
|
|
419
|
+
try {
|
|
420
|
+
return decodeURI(source)
|
|
421
|
+
} catch {
|
|
422
|
+
return undefined
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function environmentAssetSources(
|
|
427
|
+
code: string,
|
|
428
|
+
id: string,
|
|
429
|
+
emitted = false,
|
|
430
|
+
): Promise<readonly string[]> {
|
|
431
|
+
const [path] = id.split('?')
|
|
432
|
+
if (
|
|
433
|
+
path === undefined ||
|
|
434
|
+
(!/\.[cm]?[jt]sx?$/.test(path) && !/[?&]html-proxy(?:[=&]|$)/.test(id))
|
|
435
|
+
) {
|
|
436
|
+
return []
|
|
437
|
+
}
|
|
438
|
+
const sources: string[] = []
|
|
439
|
+
const transformed = await transformWithOxc(code, path)
|
|
440
|
+
const visitor = new Visitor({
|
|
441
|
+
ImportExpression(node) {
|
|
442
|
+
if (emitted) return
|
|
443
|
+
let value: string | undefined
|
|
444
|
+
if (node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
445
|
+
value = node.source.value
|
|
446
|
+
} else if (node.source.type === 'TemplateLiteral' && node.source.expressions.length === 0) {
|
|
447
|
+
value = node.source.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join('')
|
|
448
|
+
} else {
|
|
449
|
+
throw new Error(
|
|
450
|
+
'[orkestrel-environment-boundary] Dynamic imports must use static string values',
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
const decoded = decodeAssetSource(value)
|
|
454
|
+
if (decoded === undefined) {
|
|
455
|
+
throw new Error('[orkestrel-environment-boundary] Module URLs must use valid URI encoding')
|
|
456
|
+
}
|
|
457
|
+
sources.push(decoded)
|
|
458
|
+
},
|
|
459
|
+
NewExpression(node) {
|
|
460
|
+
if (emitted) return
|
|
461
|
+
const [source, base] = node.arguments
|
|
462
|
+
if (
|
|
463
|
+
node.callee.type !== 'Identifier' ||
|
|
464
|
+
node.callee.name !== 'URL' ||
|
|
465
|
+
base?.type !== 'MemberExpression' ||
|
|
466
|
+
base.object.type !== 'MetaProperty' ||
|
|
467
|
+
base.object.meta.name !== 'import' ||
|
|
468
|
+
base.object.property.name !== 'meta' ||
|
|
469
|
+
base.property.type !== 'Identifier' ||
|
|
470
|
+
base.property.name !== 'url'
|
|
471
|
+
) {
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
let value: string | undefined
|
|
475
|
+
if (source?.type === 'Literal' && typeof source.value === 'string') {
|
|
476
|
+
const decoded = decodeAssetSource(source.value)
|
|
477
|
+
if (decoded === undefined) {
|
|
478
|
+
throw new Error('[orkestrel-environment-boundary] Asset URLs must use valid URI encoding')
|
|
479
|
+
}
|
|
480
|
+
value = decoded
|
|
481
|
+
} else if (source?.type === 'TemplateLiteral') {
|
|
482
|
+
const decodedQuasis: string[] = []
|
|
483
|
+
for (const quasi of source.quasis) {
|
|
484
|
+
const decoded = decodeAssetSource(quasi.value.cooked ?? quasi.value.raw)
|
|
485
|
+
if (decoded === undefined) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
'[orkestrel-environment-boundary] Asset URLs must use valid URI encoding',
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
decodedQuasis.push(decoded)
|
|
491
|
+
}
|
|
492
|
+
if (source.expressions.length > 0) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
'[orkestrel-environment-boundary] Asset URLs must use static string values',
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
value = decodedQuasis.join('__orkestrel__')
|
|
498
|
+
} else {
|
|
499
|
+
throw new Error('[orkestrel-environment-boundary] Asset URLs must use static string values')
|
|
500
|
+
}
|
|
501
|
+
if (value === undefined) return
|
|
502
|
+
if (
|
|
503
|
+
value.startsWith('.') ||
|
|
504
|
+
value.startsWith('/') ||
|
|
505
|
+
/^file:/i.test(value) ||
|
|
506
|
+
/^[A-Za-z]:[\\/]/.test(value)
|
|
507
|
+
) {
|
|
508
|
+
sources.push(value)
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
})
|
|
512
|
+
visitor.visit(parseSync(path, transformed.code).program)
|
|
513
|
+
return sources
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function environmentBoundary(
|
|
517
|
+
owner: 'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server',
|
|
518
|
+
): Plugin {
|
|
519
|
+
const trustedPackageRoots = new Set<string>()
|
|
520
|
+
let environmentRoot = WORKSPACE_ROOT
|
|
521
|
+
return {
|
|
522
|
+
name: 'orkestrel-environment-boundary',
|
|
523
|
+
enforce: 'pre',
|
|
524
|
+
configResolved(config) {
|
|
525
|
+
environmentRoot = physicalPath(config.root)
|
|
526
|
+
},
|
|
527
|
+
async resolveId(source, importer) {
|
|
528
|
+
if (importer === undefined || !isWorkspaceBoundaryModule(importer)) return null
|
|
529
|
+
if (isBoundaryExemptModule(source)) return null
|
|
530
|
+
const normalizedSource = source.replaceAll('\\', '/')
|
|
531
|
+
const sourceError = environmentSourceError(owner, normalizedSource)
|
|
532
|
+
if (sourceError !== undefined) this.error(sourceError)
|
|
533
|
+
const importerPath = workspacePath(importer)
|
|
534
|
+
const physicalImporter = physicalPath(importer)
|
|
535
|
+
const importerPackageRoot = trustedPackageRootFor(physicalImporter, trustedPackageRoots)
|
|
536
|
+
if (importerPath === undefined && importerPackageRoot === undefined) return null
|
|
537
|
+
const [layer, environment] = importerPath?.split('/') ?? []
|
|
538
|
+
if (
|
|
539
|
+
importerPackageRoot === undefined &&
|
|
540
|
+
((layer !== 'app' && layer !== 'src') ||
|
|
541
|
+
(environment !== 'core' && environment !== 'browser' && environment !== 'server'))
|
|
542
|
+
) {
|
|
543
|
+
return null
|
|
544
|
+
}
|
|
545
|
+
const pathLike =
|
|
546
|
+
normalizedSource.startsWith('.') ||
|
|
547
|
+
normalizedSource.startsWith('/') ||
|
|
548
|
+
/^file:/i.test(normalizedSource) ||
|
|
549
|
+
/^[A-Za-z]:[\\/]/.test(normalizedSource)
|
|
550
|
+
const fallbackSource =
|
|
551
|
+
normalizedSource.startsWith('.') ||
|
|
552
|
+
normalizedSource.startsWith('/') ||
|
|
553
|
+
/^file:/i.test(normalizedSource)
|
|
554
|
+
? sourceFallback(physicalImporter, normalizedSource)
|
|
555
|
+
: ''
|
|
556
|
+
const resolution = await this.resolve(source, importer, { skipSelf: true })
|
|
557
|
+
const [resolvedId] = resolution?.id.split('?') ?? []
|
|
558
|
+
const physicalResolution = resolvedId === undefined ? undefined : physicalPath(resolvedId)
|
|
559
|
+
if (
|
|
560
|
+
importerPackageRoot !== undefined &&
|
|
561
|
+
(pathLike || normalizedSource.startsWith('#')) &&
|
|
562
|
+
physicalResolution !== undefined &&
|
|
563
|
+
!containedPath(importerPackageRoot, physicalResolution)
|
|
564
|
+
) {
|
|
565
|
+
if (normalizedSource.startsWith('#')) {
|
|
566
|
+
const mappedPackageRoot =
|
|
567
|
+
workspacePath(physicalResolution) === undefined
|
|
568
|
+
? packageRootForResolved(physicalResolution)
|
|
569
|
+
: undefined
|
|
570
|
+
if (mappedPackageRoot === undefined) {
|
|
571
|
+
return this.error(
|
|
572
|
+
'Dependency package imports must resolve inside an exact physical package root',
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
trustedPackageRoots.add(mappedPackageRoot)
|
|
576
|
+
} else {
|
|
577
|
+
this.error('Dependency modules cannot import files outside their physical package root')
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (
|
|
581
|
+
!pathLike &&
|
|
582
|
+
!normalizedSource.startsWith('#') &&
|
|
583
|
+
physicalResolution !== undefined &&
|
|
584
|
+
workspacePath(physicalResolution) === undefined
|
|
585
|
+
) {
|
|
586
|
+
const packageName = packageNameOf(normalizedSource)
|
|
587
|
+
const packageRoot =
|
|
588
|
+
packageName === undefined ? undefined : packageRootOf(packageName, physicalResolution)
|
|
589
|
+
if (packageRoot === undefined || !containedPath(packageRoot, physicalResolution)) {
|
|
590
|
+
return this.error('Resolved dependencies must remain inside their physical package root')
|
|
591
|
+
}
|
|
592
|
+
trustedPackageRoots.add(packageRoot)
|
|
593
|
+
}
|
|
594
|
+
const resolvedSource = workspacePath(resolution?.id ?? fallbackSource)
|
|
595
|
+
if (pathLike && resolvedSource === undefined && importerPackageRoot === undefined) {
|
|
596
|
+
this.error('Environment modules cannot import files outside the workspace')
|
|
597
|
+
}
|
|
598
|
+
const pathError =
|
|
599
|
+
resolvedSource === undefined
|
|
600
|
+
? undefined
|
|
601
|
+
: environmentPathError(`${layer}/${environment}`, resolvedSource)
|
|
602
|
+
if (pathError !== undefined) this.error(pathError)
|
|
603
|
+
return null
|
|
604
|
+
},
|
|
605
|
+
async load(id) {
|
|
606
|
+
if (!isWorkspaceBoundaryModule(id)) return null
|
|
607
|
+
const physicalImporter = physicalPath(id)
|
|
608
|
+
const trustedPackageRoot = trustedPackageRootFor(physicalImporter, trustedPackageRoots)
|
|
609
|
+
const inferredPackageRoot =
|
|
610
|
+
trustedPackageRoot === undefined ? packageRootForResolved(physicalImporter) : undefined
|
|
611
|
+
const packageRoot =
|
|
612
|
+
trustedPackageRoot ??
|
|
613
|
+
(inferredPackageRoot !== undefined && isPackageBoundary(inferredPackageRoot)
|
|
614
|
+
? inferredPackageRoot
|
|
615
|
+
: undefined)
|
|
616
|
+
if (packageRoot === undefined || !/\.[cm]?[jt]sx?$/.test(physicalImporter)) {
|
|
617
|
+
return null
|
|
618
|
+
}
|
|
619
|
+
const code = readBoundedFile(physicalImporter, ENVIRONMENT_MODULE_BYTES)
|
|
620
|
+
if (code === undefined) {
|
|
621
|
+
return this.error('Dependency module source must be a bounded regular file')
|
|
622
|
+
}
|
|
623
|
+
for (const source of await environmentAssetSources(code, id)) {
|
|
624
|
+
const normalizedSource = source.replaceAll('\\', '/')
|
|
625
|
+
const sourceError = environmentSourceError(owner, normalizedSource)
|
|
626
|
+
if (sourceError !== undefined) this.error(sourceError)
|
|
627
|
+
const sourcePathError = environmentPathError(owner, normalizedSource)
|
|
628
|
+
if (sourcePathError !== undefined) this.error(sourcePathError)
|
|
629
|
+
const pathLike =
|
|
630
|
+
normalizedSource.startsWith('.') ||
|
|
631
|
+
normalizedSource.startsWith('/') ||
|
|
632
|
+
/^file:/i.test(normalizedSource) ||
|
|
633
|
+
/^[A-Za-z]:[\\/]/.test(normalizedSource)
|
|
634
|
+
if (
|
|
635
|
+
pathLike &&
|
|
636
|
+
!containedPath(
|
|
637
|
+
packageRoot,
|
|
638
|
+
physicalPath(sourceFallback(physicalImporter, normalizedSource)),
|
|
639
|
+
)
|
|
640
|
+
) {
|
|
641
|
+
this.error('Dependency modules cannot import files outside their physical package root')
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return null
|
|
645
|
+
},
|
|
646
|
+
async generateBundle(_options, bundle) {
|
|
647
|
+
for (const output of Object.values(bundle)) {
|
|
648
|
+
if (output.type === 'chunk') {
|
|
649
|
+
for (const source of await environmentAssetSources(
|
|
650
|
+
output.code,
|
|
651
|
+
output.fileName.endsWith('.js') ? output.fileName : `${output.fileName}.js`,
|
|
652
|
+
true,
|
|
653
|
+
)) {
|
|
654
|
+
const normalizedSource = source.replaceAll('\\', '/')
|
|
655
|
+
const sourceError = environmentSourceError(owner, normalizedSource)
|
|
656
|
+
if (sourceError !== undefined) this.error(sourceError)
|
|
657
|
+
}
|
|
658
|
+
continue
|
|
659
|
+
}
|
|
660
|
+
for (const original of output.originalFileNames) {
|
|
661
|
+
const physical = physicalPath(
|
|
662
|
+
isAbsolute(original) ? original : resolvePath(environmentRoot, original),
|
|
663
|
+
)
|
|
664
|
+
if (isBoundaryExemptModule(original) || isBoundaryExemptModule(physical)) continue
|
|
665
|
+
const target = workspacePath(physical)
|
|
666
|
+
if (target === undefined) {
|
|
667
|
+
if (trustedPackageRootFor(physical, trustedPackageRoots) === undefined) {
|
|
668
|
+
this.error('Environment modules cannot import files outside the workspace')
|
|
669
|
+
}
|
|
670
|
+
continue
|
|
671
|
+
}
|
|
672
|
+
const pathError = environmentPathError(owner, target)
|
|
673
|
+
if (pathError !== undefined) this.error(pathError)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
buildEnd(error) {
|
|
678
|
+
if (error !== undefined) return
|
|
679
|
+
for (const id of this.getModuleIds()) {
|
|
680
|
+
if (!isWorkspaceBoundaryModule(id)) continue
|
|
681
|
+
const target = workspacePath(id)
|
|
682
|
+
if (target === undefined) {
|
|
683
|
+
if (
|
|
684
|
+
isOutsideWorkspacePath(id) &&
|
|
685
|
+
trustedPackageRootFor(physicalPath(id), trustedPackageRoots) === undefined
|
|
686
|
+
) {
|
|
687
|
+
this.error('Environment modules cannot import files outside the workspace')
|
|
688
|
+
}
|
|
689
|
+
continue
|
|
690
|
+
}
|
|
691
|
+
const pathError = environmentPathError(owner, target)
|
|
692
|
+
if (pathError !== undefined) this.error(pathError)
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
transform: {
|
|
696
|
+
order: 'pre',
|
|
697
|
+
async handler(code, id) {
|
|
698
|
+
if (!isWorkspaceBoundaryModule(id)) return null
|
|
699
|
+
const target = workspacePath(id)
|
|
700
|
+
const physicalImporter = physicalPath(id)
|
|
701
|
+
const importerPackageRoot = trustedPackageRootFor(physicalImporter, trustedPackageRoots)
|
|
702
|
+
if (target === undefined) {
|
|
703
|
+
if (isOutsideWorkspacePath(id) && importerPackageRoot === undefined) {
|
|
704
|
+
this.error('Environment modules cannot import files outside the workspace')
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
const pathError = environmentPathError(owner, target)
|
|
708
|
+
if (pathError !== undefined) this.error(pathError)
|
|
709
|
+
}
|
|
710
|
+
const environmentModule =
|
|
711
|
+
target !== undefined && /^(?:app|src)\/(?:core|browser|server)\//.test(target)
|
|
712
|
+
if (!environmentModule && importerPackageRoot === undefined) return null
|
|
713
|
+
for (const source of await environmentAssetSources(code, id)) {
|
|
714
|
+
const normalizedSource = source.replaceAll('\\', '/')
|
|
715
|
+
const sourceError = environmentSourceError(owner, normalizedSource)
|
|
716
|
+
if (sourceError !== undefined) this.error(sourceError)
|
|
717
|
+
const [sourcePath] = normalizedSource.split(/[?#]/)
|
|
718
|
+
if (sourcePath !== undefined && isBuiltin(sourcePath)) continue
|
|
719
|
+
const resolution = await this.resolve(normalizedSource, id, { skipSelf: true })
|
|
720
|
+
const fallbackSource = sourceFallback(physicalImporter, normalizedSource)
|
|
721
|
+
const physicalSource = physicalPath(resolution?.id ?? fallbackSource)
|
|
722
|
+
if (importerPackageRoot !== undefined) {
|
|
723
|
+
const pathLike =
|
|
724
|
+
normalizedSource.startsWith('.') ||
|
|
725
|
+
normalizedSource.startsWith('/') ||
|
|
726
|
+
/^file:/i.test(normalizedSource) ||
|
|
727
|
+
/^[A-Za-z]:[\\/]/.test(normalizedSource)
|
|
728
|
+
if (pathLike && !containedPath(importerPackageRoot, physicalSource)) {
|
|
729
|
+
this.error(
|
|
730
|
+
'Dependency modules cannot import files outside their physical package root',
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
if (!pathLike && !containedPath(importerPackageRoot, physicalSource)) {
|
|
734
|
+
const packageName = packageNameOf(normalizedSource)
|
|
735
|
+
const packageRoot = normalizedSource.startsWith('#')
|
|
736
|
+
? workspacePath(physicalSource) === undefined
|
|
737
|
+
? packageRootForResolved(physicalSource)
|
|
738
|
+
: undefined
|
|
739
|
+
: packageName === undefined
|
|
740
|
+
? undefined
|
|
741
|
+
: packageRootOf(packageName, physicalSource)
|
|
742
|
+
if (packageRoot === undefined || !containedPath(packageRoot, physicalSource)) {
|
|
743
|
+
return this.error(
|
|
744
|
+
'Resolved dependencies must remain inside their physical package root',
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
trustedPackageRoots.add(packageRoot)
|
|
748
|
+
}
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
const resolvedSource = workspacePath(physicalSource)
|
|
752
|
+
if (resolvedSource === undefined) {
|
|
753
|
+
return this.error('Environment modules cannot import files outside the workspace')
|
|
754
|
+
}
|
|
755
|
+
const assetError = environmentPathError(owner, resolvedSource)
|
|
756
|
+
if (assetError !== undefined) this.error(assetError)
|
|
757
|
+
}
|
|
758
|
+
return null
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
}
|
|
762
|
+
}
|