@prestack/thread 0.1.21 → 0.1.22
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/CHANGELOG.md +109 -0
- package/dist/core/attach.js +21 -0
- package/dist/core/descend.js +6 -3
- package/dist/core/generators/commit.js +55 -3
- package/dist/core/generators/dev.d.ts +1 -0
- package/dist/core/generators/dev.js +68 -0
- package/dist/core/tree.d.ts +1 -0
- package/dist/core/tree.js +7 -1
- package/dist/derivation/generators/build.d.ts +3 -1
- package/dist/derivation/generators/build.js +103 -22
- package/dist/derivation/generators/release.js +74 -26
- package/dist/derivation/release.js +3 -3
- package/dist/integrations/cli.js +69 -10
- package/dist/integrations/pglite.js +13 -3
- package/dist/integrations/target.d.ts +3 -0
- package/dist/integrity/attach.js +1 -5
- package/dist/integrity/slopnators/sl_stale_path.js +10 -0
- package/dist/operations/access.js +3 -2
- package/dist/operations/capabilities.d.ts +2 -0
- package/dist/operations/capabilities.js +2 -0
- package/dist/operations/generators/capability.js +234 -24
- package/dist/operations/generators/compile.js +150 -50
- package/dist/operations/generators/seed.d.ts +3 -0
- package/dist/operations/generators/seed.js +25 -3
- package/dist/operations/nodes.d.ts +2 -2
- package/package.json +1 -1
- package/dist/core/actor.ts +0 -21
- package/dist/core/attach.ts +0 -613
- package/dist/core/chain.ts +0 -145
- package/dist/core/condition.ts +0 -227
- package/dist/core/descend.ts +0 -1185
- package/dist/core/errors/catalogue.ts +0 -314
- package/dist/core/errors/classes.ts +0 -51
- package/dist/core/errors/error.ts +0 -152
- package/dist/core/errors/namespaces.ts +0 -19
- package/dist/core/finding.ts +0 -37
- package/dist/core/generators/commit.ts +0 -157
- package/dist/core/init.ts +0 -222
- package/dist/core/invoked.ts +0 -52
- package/dist/core/round.ts +0 -74
- package/dist/core/rows.ts +0 -506
- package/dist/core/scope.ts +0 -20
- package/dist/core/thread.ts +0 -29
- package/dist/core/tree.ts +0 -90
- package/dist/core/types.ts +0 -351
- package/dist/derivation/attach.ts +0 -57
- package/dist/derivation/generators/build.ts +0 -371
- package/dist/derivation/generators/check.ts +0 -118
- package/dist/derivation/generators/determinism.ts +0 -145
- package/dist/derivation/generators/release.ts +0 -236
- package/dist/derivation/release.ts +0 -43
- package/dist/integrations/attach.ts +0 -187
- package/dist/integrations/cli.ts +0 -606
- package/dist/integrations/declared.ts +0 -34
- package/dist/integrations/disk.ts +0 -466
- package/dist/integrations/generators/read.ts +0 -127
- package/dist/integrations/generators/vault.ts +0 -210
- package/dist/integrations/https.ts +0 -216
- package/dist/integrations/pglite.ts +0 -206
- package/dist/integrations/secret.ts +0 -54
- package/dist/integrations/target.ts +0 -672
- package/dist/integrations/vault.ts +0 -148
- package/dist/integrity/attach.ts +0 -150
- package/dist/integrity/check-threads.ts +0 -166
- package/dist/integrity/constraint.ts +0 -1246
- package/dist/integrity/generators/compiler.ts +0 -138
- package/dist/integrity/generators/gate.ts +0 -227
- package/dist/integrity/generators/issues.ts +0 -160
- package/dist/integrity/generators/publish.ts +0 -233
- package/dist/integrity/generators/trial.ts +0 -359
- package/dist/integrity/lint.ts +0 -214
- package/dist/integrity/lsp/bin.ts +0 -44
- package/dist/integrity/lsp/diagnostics.ts +0 -92
- package/dist/integrity/lsp/findings.ts +0 -63
- package/dist/integrity/lsp/lint.ts +0 -124
- package/dist/integrity/lsp/protocol.ts +0 -78
- package/dist/integrity/lsp/server.ts +0 -224
- package/dist/integrity/slopnator.ts +0 -324
- package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
- package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
- package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
- package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
- package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
- package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
- package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
- package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
- package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
- package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
- package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
- package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
- package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
- package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
- package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
- package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
- package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
- package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
- package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
- package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
- package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
- package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
- package/dist/integrity/sweep.ts +0 -395
- package/dist/knowledge/attach.ts +0 -327
- package/dist/knowledge/generators/docsgen.ts +0 -186
- package/dist/knowledge/generators/glossary.ts +0 -73
- package/dist/knowledge/glossary.ts +0 -6
- package/dist/operations/access.ts +0 -131
- package/dist/operations/attach.ts +0 -408
- package/dist/operations/capabilities.ts +0 -88
- package/dist/operations/dispatch.ts +0 -294
- package/dist/operations/error-results.ts +0 -31
- package/dist/operations/generators/capability.ts +0 -1054
- package/dist/operations/generators/compile.ts +0 -701
- package/dist/operations/generators/permits.ts +0 -229
- package/dist/operations/generators/seed.ts +0 -359
- package/dist/operations/generators/trial.ts +0 -666
- package/dist/operations/nodes.ts +0 -53
- package/dist/operations/operations.ts +0 -91
- package/dist/operations/result.ts +0 -28
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
import { builtinModules, createRequire } from 'node:module'
|
|
2
|
-
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
3
|
-
import { argv, cwd, stderr, stdout } from 'node:process'
|
|
4
|
-
import { pathToFileURL } from 'node:url'
|
|
5
|
-
|
|
6
|
-
import { build, initialize, stop, type BuildOptions, type Loader, type Plugin } from 'esbuild-wasm/esm/browser.js'
|
|
7
|
-
|
|
8
|
-
import { descend, reached, type Pass } from '../../core/descend.ts'
|
|
9
|
-
import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from '../../integrations/disk.ts'
|
|
10
|
-
import { exit, invoked } from '../../core/invoked.ts'
|
|
11
|
-
import { valuesIn } from '../../core/rows.ts'
|
|
12
|
-
import { findTree } from '../../core/tree.ts'
|
|
13
|
-
import type { Secret } from '../../integrations/secret.ts'
|
|
14
|
-
import { servesBrowser, type Target } from '../../integrations/target.ts'
|
|
15
|
-
|
|
16
|
-
const TREE = findTree()
|
|
17
|
-
|
|
18
|
-
const ROOT = TREE.at
|
|
19
|
-
|
|
20
|
-
const OUTSIDE = /(^|\/)node_modules(\/|$)/
|
|
21
|
-
|
|
22
|
-
const BUILDS = 'thread build'
|
|
23
|
-
|
|
24
|
-
const here = (path: string): string => relative(ROOT, path).split('\\').join('/')
|
|
25
|
-
|
|
26
|
-
const LOADERS: Readonly<Record<string, Loader>> = {
|
|
27
|
-
'.ts': 'ts',
|
|
28
|
-
'.mts': 'ts',
|
|
29
|
-
'.cts': 'ts',
|
|
30
|
-
'.tsx': 'tsx',
|
|
31
|
-
'.js': 'js',
|
|
32
|
-
'.mjs': 'js',
|
|
33
|
-
'.cjs': 'js',
|
|
34
|
-
'.jsx': 'jsx',
|
|
35
|
-
'.json': 'json',
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const TRIED = Object.keys(LOADERS)
|
|
39
|
-
|
|
40
|
-
const IMPORTED = ['import', 'module', 'node', 'default']
|
|
41
|
-
|
|
42
|
-
const REQUIRED = ['require', 'module', 'node', 'default']
|
|
43
|
-
|
|
44
|
-
const RUNTIME = new Set(builtinModules)
|
|
45
|
-
|
|
46
|
-
const WASM = createRequire(import.meta.url).resolve('esbuild-wasm/esbuild.wasm')
|
|
47
|
-
|
|
48
|
-
let compiled: WebAssembly.Module | undefined
|
|
49
|
-
|
|
50
|
-
const filed = (at: string): boolean => statSync(at, { throwIfNoEntry: false })?.isFile() === true
|
|
51
|
-
|
|
52
|
-
const inside = (top: string, at: string): boolean => {
|
|
53
|
-
const said = relative(top, at)
|
|
54
|
-
return !said.startsWith('..') && !isAbsolute(said)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function located(base: string): string | undefined {
|
|
58
|
-
const found = [base, ...TRIED.map((one) => base + one), ...TRIED.map((one) => join(base, `index${one}`))].find(filed)
|
|
59
|
-
return found === undefined ? undefined : realpathSync(found)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function chosen(held: unknown, conditions: readonly string[]): string | undefined {
|
|
63
|
-
if (typeof held === 'string') return held
|
|
64
|
-
if (Array.isArray(held)) return held.map((one: unknown) => chosen(one, conditions)).find((one) => one !== undefined)
|
|
65
|
-
if (typeof held !== 'object' || held === null) return undefined
|
|
66
|
-
for (const [condition, one] of Object.entries(held as Readonly<Record<string, unknown>>)) {
|
|
67
|
-
if (!conditions.includes(condition)) continue
|
|
68
|
-
const found = chosen(one, conditions)
|
|
69
|
-
if (found !== undefined) return found
|
|
70
|
-
}
|
|
71
|
-
return undefined
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function exported(held: unknown, sub: string, conditions: readonly string[]): string | undefined {
|
|
75
|
-
if (typeof held !== 'object' || held === null || Array.isArray(held) || !Object.keys(held).some((one) => one.startsWith('.'))) {
|
|
76
|
-
return sub === '.' ? chosen(held, conditions) : undefined
|
|
77
|
-
}
|
|
78
|
-
const paths = held as Readonly<Record<string, unknown>>
|
|
79
|
-
if (sub in paths) return chosen(paths[sub], conditions)
|
|
80
|
-
for (const [key, one] of Object.entries(paths)) {
|
|
81
|
-
const star = key.indexOf('*')
|
|
82
|
-
if (star === -1) continue
|
|
83
|
-
const before = key.slice(0, star)
|
|
84
|
-
const after = key.slice(star + 1)
|
|
85
|
-
if (sub.length < key.length - 1 || !sub.startsWith(before) || !sub.endsWith(after)) continue
|
|
86
|
-
return chosen(one, conditions)?.split('*').join(sub.slice(before.length, sub.length - after.length))
|
|
87
|
-
}
|
|
88
|
-
return undefined
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function packaged(specifier: string, from: string, conditions: readonly string[], top: string): string | undefined {
|
|
92
|
-
const name = specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/')
|
|
93
|
-
const sub = `.${specifier.slice(name.length)}`
|
|
94
|
-
for (let at = from; inside(top, at); at = dirname(at)) {
|
|
95
|
-
const root = join(at, 'node_modules', name)
|
|
96
|
-
const manifest = join(root, 'package.json')
|
|
97
|
-
if (filed(manifest)) {
|
|
98
|
-
const held = JSON.parse(readFileSync(manifest, 'utf8')) as { readonly exports?: unknown; readonly main?: unknown; readonly module?: unknown }
|
|
99
|
-
if (held.exports !== undefined) {
|
|
100
|
-
const found = exported(held.exports, sub, conditions)
|
|
101
|
-
return found === undefined ? undefined : located(join(root, found))
|
|
102
|
-
}
|
|
103
|
-
const main = typeof held.main === 'string' ? held.main : typeof held.module === 'string' ? held.module : 'index.js'
|
|
104
|
-
return located(join(root, sub === '.' ? main : sub))
|
|
105
|
-
}
|
|
106
|
-
if (at === top) return undefined
|
|
107
|
-
}
|
|
108
|
-
return undefined
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const folder = (outside: boolean, top: string): Plugin => ({
|
|
112
|
-
name: 'thread-folder',
|
|
113
|
-
setup(held) {
|
|
114
|
-
held.onResolve({ filter: /.*/ }, (args) => {
|
|
115
|
-
const bare = !args.path.startsWith('.') && !isAbsolute(args.path)
|
|
116
|
-
if (args.path.startsWith('node:') || RUNTIME.has(args.path)) return { path: args.path, external: true, sideEffects: false }
|
|
117
|
-
if (bare && outside) return { path: args.path, external: true }
|
|
118
|
-
const conditions = args.kind === 'require-call' || args.kind === 'require-resolve' ? REQUIRED : IMPORTED
|
|
119
|
-
const path = resolve(args.resolveDir, args.path)
|
|
120
|
-
const found = bare ? packaged(args.path, args.resolveDir, conditions, top) : inside(top, path) ? located(path) : undefined
|
|
121
|
-
if (found !== undefined) return { path: found }
|
|
122
|
-
if (args.kind === 'require-call') return { path: args.path, external: true }
|
|
123
|
-
return {
|
|
124
|
-
errors: [
|
|
125
|
-
{
|
|
126
|
-
text:
|
|
127
|
-
`${args.path} is imported by ${args.importer === '' ? 'the entry' : args.importer}, and nothing in ${top} answers ` +
|
|
128
|
-
'to that name, so there is nothing to bundle in its place. A bundle reads only the folder it is built in',
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
held.onLoad({ filter: /.*/ }, (args) => ({
|
|
134
|
-
contents: readFileSync(args.path, 'utf8'),
|
|
135
|
-
loader: LOADERS[extname(args.path)] ?? 'js',
|
|
136
|
-
resolveDir: dirname(args.path),
|
|
137
|
-
}))
|
|
138
|
-
},
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
export async function bundle(options: BuildOptions): Promise<string> {
|
|
142
|
-
compiled ??= new WebAssembly.Module(Buffer.from(readFileSync(WASM, 'latin1'), 'latin1'))
|
|
143
|
-
const hosted = globalThis as { self?: unknown }
|
|
144
|
-
hosted.self ??= globalThis
|
|
145
|
-
await initialize({ wasmModule: compiled, worker: false })
|
|
146
|
-
try {
|
|
147
|
-
const { packages, plugins, absWorkingDir = cwd(), ...rest } = options
|
|
148
|
-
const made = await build({
|
|
149
|
-
absWorkingDir,
|
|
150
|
-
...rest,
|
|
151
|
-
bundle: true,
|
|
152
|
-
write: false,
|
|
153
|
-
plugins: [...(plugins ?? []), folder(packages === 'external', absWorkingDir)],
|
|
154
|
-
})
|
|
155
|
-
const text = made.outputFiles?.[0]?.text
|
|
156
|
-
if (text === undefined) throw new Error('the bundle came back holding no file, so there is nothing to write where it goes')
|
|
157
|
-
return text
|
|
158
|
-
} finally {
|
|
159
|
-
await stop()
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function wanted(pass: Pass): readonly { entry: string; out: string }[] {
|
|
164
|
-
|
|
165
|
-
const owns = new Map<string, string>()
|
|
166
|
-
for (const [at, threadings] of pass.threaded) {
|
|
167
|
-
for (const one of threadings) {
|
|
168
|
-
const id = one.row.id
|
|
169
|
-
if (id !== undefined && !owns.has(id)) owns.set(id, at)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const found: { entry: string; out: string }[] = []
|
|
174
|
-
for (const [at, threadings] of pass.threaded) {
|
|
175
|
-
for (const one of threadings) {
|
|
176
|
-
if (one.list !== 'derives' || one.row.by !== BUILDS) continue
|
|
177
|
-
const entry = owns.get(one.by)
|
|
178
|
-
if (entry !== undefined) found.push({ entry, out: at })
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return found
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function compilers(pass: Pass): readonly string[] {
|
|
185
|
-
const found: string[] = []
|
|
186
|
-
for (const [at, threadings] of pass.threaded) {
|
|
187
|
-
for (const one of threadings) {
|
|
188
|
-
if (one.list !== 'declares' || one.row.by !== BUILDS) continue
|
|
189
|
-
if (!found.includes(at)) found.push(at)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return found
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const JSX = /\.(c|m)?(t|j)sx$/
|
|
196
|
-
|
|
197
|
-
function browsing(entry: string): boolean {
|
|
198
|
-
return JSX.test(entry)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function sheet<T>(file: string, list: string): readonly T[] {
|
|
202
|
-
const path = resolve(ROOT, `${TREE.name}/${file}`)
|
|
203
|
-
if (!existsSync(path)) return []
|
|
204
|
-
try {
|
|
205
|
-
const held = valuesIn(readFileSync(path, 'utf8')) as Record<string, readonly T[] | undefined>
|
|
206
|
-
return held[list] ?? []
|
|
207
|
-
} catch {
|
|
208
|
-
return []
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function withheld(pass: Pass): ReadonlyMap<string, string> {
|
|
213
|
-
const targets = sheet<Target>('targets.ts', 'TARGETS')
|
|
214
|
-
if (targets.length === 0) return new Map()
|
|
215
|
-
const secrets = sheet<Secret>('secrets.ts', 'SECRETS')
|
|
216
|
-
|
|
217
|
-
const because = new Map<string, string>()
|
|
218
|
-
for (const one of targets) {
|
|
219
|
-
const said = servesBrowser(one, secrets)
|
|
220
|
-
if (said !== true) because.set(one.id, said)
|
|
221
|
-
}
|
|
222
|
-
if (because.size === 0) return new Map()
|
|
223
|
-
|
|
224
|
-
const guilty = new Map<string, string>()
|
|
225
|
-
const consider = (row: { readonly id?: string; readonly requires?: readonly string[] }): void => {
|
|
226
|
-
if (row.id === undefined || row.requires === undefined) return
|
|
227
|
-
for (const word of row.requires) {
|
|
228
|
-
const said = because.get(word)
|
|
229
|
-
if (said === undefined) continue
|
|
230
|
-
guilty.set(row.id, `\`${row.id}\` requires \`${word}\`, and that target cannot be reached from a page. ${said}`)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
for (const one of pass.stated.values()) consider(one)
|
|
234
|
-
for (const held of pass.threaded.values()) for (const each of held) consider(each.row)
|
|
235
|
-
if (guilty.size === 0) return new Map()
|
|
236
|
-
|
|
237
|
-
const found = new Map<string, string>()
|
|
238
|
-
for (const [at, held] of pass.threaded) {
|
|
239
|
-
for (const each of held) {
|
|
240
|
-
const said = guilty.get(each.by)
|
|
241
|
-
if (said !== undefined) found.set(resolve(ROOT, at), said)
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return found
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function withholding(refusals: ReadonlyMap<string, string>): {
|
|
248
|
-
readonly name: string
|
|
249
|
-
readonly setup: (build: { onLoad: (opts: { filter: RegExp }, run: (args: { path: string }) => unknown) => void }) => void
|
|
250
|
-
} {
|
|
251
|
-
return {
|
|
252
|
-
name: 'thread-withheld',
|
|
253
|
-
setup(build) {
|
|
254
|
-
build.onLoad({ filter: /.*/ }, (args) => {
|
|
255
|
-
const said = refusals.get(args.path)
|
|
256
|
-
if (said === undefined) return undefined
|
|
257
|
-
return {
|
|
258
|
-
errors: [
|
|
259
|
-
{
|
|
260
|
-
text:
|
|
261
|
-
`${said} This entry is JSX, so it is built for a browser, and everything a browser bundle ` +
|
|
262
|
-
'reaches is in the bundle. Move the operation behind something the page calls, or take the ' +
|
|
263
|
-
'target off its row.',
|
|
264
|
-
},
|
|
265
|
-
],
|
|
266
|
-
}
|
|
267
|
-
})
|
|
268
|
-
},
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function declared(): { readonly name: string; readonly setup: (build: { onLoad: (opts: { filter: RegExp }, run: (args: { path: string }) => unknown) => void }) => void } {
|
|
273
|
-
const wired = new Set(reached())
|
|
274
|
-
return {
|
|
275
|
-
name: 'thread-declared',
|
|
276
|
-
setup(held) {
|
|
277
|
-
held.onLoad({ filter: /\.(c|m)?(t|j)sx?$/ }, (args) => {
|
|
278
|
-
const path = here(args.path)
|
|
279
|
-
if (OUTSIDE.test(path) || path.startsWith('..') || wired.has(path)) return undefined
|
|
280
|
-
return {
|
|
281
|
-
errors: [
|
|
282
|
-
{
|
|
283
|
-
text:
|
|
284
|
-
`${path} is not declared — the descent from ${TREE.name}/thread.ts never reaches it, so nothing ` +
|
|
285
|
-
'in this app agreed it exists. Declare it from the row that owns it, or delete it. A file nothing ' +
|
|
286
|
-
'names cannot be part of what this app ships',
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
}
|
|
290
|
-
})
|
|
291
|
-
},
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function main(): Promise<void> {
|
|
296
|
-
const pass = descend()
|
|
297
|
-
const all = wanted(pass)
|
|
298
|
-
const checking = argv.includes('--check')
|
|
299
|
-
|
|
300
|
-
const compiling: unknown[] = []
|
|
301
|
-
for (const one of compilers(pass)) {
|
|
302
|
-
const held = (await import(pathToFileURL(resolve(ROOT, one)).href)) as { plugin?: unknown }
|
|
303
|
-
if (held.plugin === undefined) {
|
|
304
|
-
stderr.write(
|
|
305
|
-
`build: ${one} is declared \`by: '${BUILDS}'\` and exports no \`plugin\`, so the build has nothing to run ` +
|
|
306
|
-
'from it. A file that says it compiles and compiles nothing is worse than one that says neither.\n',
|
|
307
|
-
)
|
|
308
|
-
exit(1)
|
|
309
|
-
}
|
|
310
|
-
compiling.push(held.plugin)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (all.length === 0) {
|
|
314
|
-
if (checking) {
|
|
315
|
-
stdout.write(`build: nothing in ${TREE.name}/ derives a file \`by: '${BUILDS}'\`, so there is no artifact to be out of date.\n`)
|
|
316
|
-
return
|
|
317
|
-
}
|
|
318
|
-
stderr.write(
|
|
319
|
-
`build: nothing in ${TREE.name}/ derives a file \`by: '${BUILDS}'\`, so this app states no artifact to build. ` +
|
|
320
|
-
'Declare one from the row that owns the entry, and the build reads its entry from that row.\n',
|
|
321
|
-
)
|
|
322
|
-
exit(1)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
for (const { entry, out } of all) {
|
|
326
|
-
const at = resolve(ROOT, out)
|
|
327
|
-
const before = checking && existsSync(at) ? readFileSync(at, 'utf8') : undefined
|
|
328
|
-
|
|
329
|
-
if (checking && before === undefined) {
|
|
330
|
-
stderr.write(`build: ${out} is declared and not on disk. Run \`${BUILDS}\` and commit what it writes.\n`)
|
|
331
|
-
exit(1)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const forBrowser = browsing(entry)
|
|
335
|
-
const refusals = forBrowser ? withheld(pass) : new Map<string, string>()
|
|
336
|
-
|
|
337
|
-
let held: string
|
|
338
|
-
try {
|
|
339
|
-
held = await bundle({
|
|
340
|
-
entryPoints: [resolve(ROOT, entry)],
|
|
341
|
-
format: 'esm',
|
|
342
|
-
platform: forBrowser ? 'browser' : 'node',
|
|
343
|
-
...(forBrowser ? {} : { packages: 'external' as const }),
|
|
344
|
-
outfile: at,
|
|
345
|
-
plugins: [declared() as never, ...(refusals.size === 0 ? [] : [withholding(refusals) as never]), ...(compiling as never[])],
|
|
346
|
-
logLevel: 'silent',
|
|
347
|
-
})
|
|
348
|
-
} catch (thrown) {
|
|
349
|
-
stderr.write(`build: ${out} was not written — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`)
|
|
350
|
-
exit(1)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (checking) {
|
|
354
|
-
if (held !== before) {
|
|
355
|
-
stderr.write(
|
|
356
|
-
`build: ${out} is not what ${BUILDS} writes. A built artifact has no author — run \`${BUILDS}\` and commit the result.\n`,
|
|
357
|
-
)
|
|
358
|
-
exit(1)
|
|
359
|
-
}
|
|
360
|
-
stdout.write(`build: ${out} is up to date — ${String(new Set(reached()).size)} declared file(s) reachable.\n`)
|
|
361
|
-
continue
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
writeFileSync(at, held, 'utf8')
|
|
365
|
-
stdout.write(`build: wrote ${out} from ${entry} — ${String(new Set(reached()).size)} declared file(s) reachable.\n`)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (invoked(import.meta.url)) await main()
|
|
370
|
-
|
|
371
|
-
export { declared, wanted }
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { bodyHash, parseGenerated, type GeneratedFile } from './determinism.ts'
|
|
2
|
-
|
|
3
|
-
export type CheckStatus = 'ok' | 'missing' | 'stale' | 'hand-edited' | 'orphaned'
|
|
4
|
-
|
|
5
|
-
export interface FileCheck {
|
|
6
|
-
readonly path: string
|
|
7
|
-
readonly status: CheckStatus
|
|
8
|
-
readonly detail: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface CheckReport {
|
|
12
|
-
readonly ok: boolean
|
|
13
|
-
readonly checks: readonly FileCheck[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type ReadFile = (path: string) => string | null
|
|
17
|
-
|
|
18
|
-
export interface CheckOptions {
|
|
19
|
-
|
|
20
|
-
readonly previouslyGenerated?: readonly string[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function checkGenerated(
|
|
24
|
-
expected: readonly GeneratedFile[],
|
|
25
|
-
read: ReadFile,
|
|
26
|
-
options: CheckOptions = {},
|
|
27
|
-
): CheckReport {
|
|
28
|
-
const checks: FileCheck[] = []
|
|
29
|
-
const expectedPaths = new Set(expected.map((file) => file.path))
|
|
30
|
-
|
|
31
|
-
for (const file of expected) {
|
|
32
|
-
const actual = read(file.path)
|
|
33
|
-
|
|
34
|
-
if (actual === null) {
|
|
35
|
-
checks.push({ path: file.path, status: 'missing', detail: 'not committed — run `thread generate`' })
|
|
36
|
-
continue
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (actual === file.contents) {
|
|
40
|
-
checks.push({ path: file.path, status: 'ok', detail: 'up to date' })
|
|
41
|
-
continue
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const parsed = parseGenerated(file.path, actual)
|
|
45
|
-
if (parsed === null) {
|
|
46
|
-
checks.push({
|
|
47
|
-
path: file.path,
|
|
48
|
-
status: 'hand-edited',
|
|
49
|
-
detail: 'the generated header is gone — this file was replaced by hand',
|
|
50
|
-
})
|
|
51
|
-
continue
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (bodyHash(parsed.body) !== parsed.declaredHash) {
|
|
55
|
-
checks.push({
|
|
56
|
-
path: file.path,
|
|
57
|
-
status: 'hand-edited',
|
|
58
|
-
detail:
|
|
59
|
-
'the body no longer matches its own header hash. Move the change into ' +
|
|
60
|
-
`${parsed.source} first — regenerating will discard it.`,
|
|
61
|
-
})
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
checks.push({
|
|
66
|
-
path: file.path,
|
|
67
|
-
status: 'stale',
|
|
68
|
-
detail: `out of date with ${parsed.source} — run \`thread generate\``,
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for (const path of options.previouslyGenerated ?? []) {
|
|
73
|
-
if (expectedPaths.has(path)) continue
|
|
74
|
-
checks.push({
|
|
75
|
-
path,
|
|
76
|
-
status: 'orphaned',
|
|
77
|
-
detail: 'generation no longer owns this file — delete it',
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const sorted = checks.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
82
|
-
return { ok: sorted.every((check) => check.status === 'ok'), checks: sorted }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function formatCheckReport(report: CheckReport): string {
|
|
86
|
-
if (report.ok) return `generated output is up to date (${report.checks.length} files).`
|
|
87
|
-
|
|
88
|
-
const problems = report.checks.filter((check) => check.status !== 'ok')
|
|
89
|
-
return [
|
|
90
|
-
`${problems.length} of ${report.checks.length} generated file(s) need attention:`,
|
|
91
|
-
...problems.map((check) => ` ${check.status.padEnd(12)} ${check.path}\n ${check.detail}`),
|
|
92
|
-
].join('\n')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function detectHandEdits(
|
|
96
|
-
paths: readonly string[],
|
|
97
|
-
read: ReadFile,
|
|
98
|
-
): readonly FileCheck[] {
|
|
99
|
-
const edited: FileCheck[] = []
|
|
100
|
-
|
|
101
|
-
for (const path of [...paths].sort()) {
|
|
102
|
-
const contents = read(path)
|
|
103
|
-
if (contents === null) continue
|
|
104
|
-
|
|
105
|
-
const parsed = parseGenerated(path, contents)
|
|
106
|
-
if (parsed === null) continue
|
|
107
|
-
|
|
108
|
-
if (bodyHash(parsed.body) !== parsed.declaredHash) {
|
|
109
|
-
edited.push({
|
|
110
|
-
path,
|
|
111
|
-
status: 'hand-edited',
|
|
112
|
-
detail: `edited by hand. The change belongs in ${parsed.source}.`,
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return edited
|
|
118
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
import { named } from '../../core/scope.ts'
|
|
4
|
-
|
|
5
|
-
export interface GeneratedFile {
|
|
6
|
-
|
|
7
|
-
readonly path: string
|
|
8
|
-
|
|
9
|
-
readonly contents: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function sortedBy<T>(items: Iterable<T>, key: (item: T) => string): T[] {
|
|
13
|
-
return [...items].sort((a, b) => compare(key(a), key(b)))
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function compare(a: string, b: string): number {
|
|
17
|
-
return a < b ? -1 : a > b ? 1 : 0
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function sortedEntries<V>(record: Readonly<Record<string, V>>): [string, V][] {
|
|
21
|
-
return Object.keys(record)
|
|
22
|
-
.sort(compare)
|
|
23
|
-
.map((key) => [key, record[key] as V])
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const BANNER = `GENERATED by ${named('derivation')} — do not edit by hand.`
|
|
27
|
-
const HASH_LABEL = 'codegen-hash:'
|
|
28
|
-
|
|
29
|
-
interface HeaderSyntax {
|
|
30
|
-
readonly wrap: (text: string) => string
|
|
31
|
-
readonly unwrap: (line: string) => string | null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function headerSyntax(path: string): HeaderSyntax {
|
|
35
|
-
if (path.endsWith('.md')) {
|
|
36
|
-
return {
|
|
37
|
-
wrap: (text) => `<!-- ${text} -->`,
|
|
38
|
-
unwrap: (line) =>
|
|
39
|
-
line.startsWith('<!-- ') && line.endsWith(' -->') ? line.slice(5, -4) : null,
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const prefix = path.endsWith('.sql') ? '--' : path.endsWith('.ts') || path.endsWith('.tsx') ? '//' : null
|
|
43
|
-
if (prefix === null) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
`${named('derivation')} emits only .ts, .sql and .md, received "${path}". ` +
|
|
46
|
-
'A format with no comment syntax cannot carry a header, and an unheadered generated ' +
|
|
47
|
-
'file is one somebody edits by hand.',
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
wrap: (text) => `${prefix} ${text}`,
|
|
52
|
-
unwrap: (line) => (line.startsWith(`${prefix} `) ? line.slice(prefix.length + 1) : null),
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function bodyHash(body: string): string {
|
|
57
|
-
return createHash('sha256').update(body, 'utf8').digest('hex')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const HEADER_LINES = 5
|
|
61
|
-
|
|
62
|
-
export function withHeader(path: string, source: string, body: string): GeneratedFile {
|
|
63
|
-
const { wrap } = headerSyntax(path)
|
|
64
|
-
const normalized = body.endsWith('\n') ? body : `${body}\n`
|
|
65
|
-
|
|
66
|
-
const header = [
|
|
67
|
-
wrap(BANNER),
|
|
68
|
-
wrap(`Source: ${source}`),
|
|
69
|
-
wrap('Edit the island and re-run `thread generate`. CI fails when this file is stale.'),
|
|
70
|
-
wrap(`${HASH_LABEL} ${bodyHash(normalized)}`),
|
|
71
|
-
'',
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
return { path, contents: `${header.join('\n')}\n${normalized}` }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface ParsedFile {
|
|
78
|
-
readonly source: string
|
|
79
|
-
readonly declaredHash: string
|
|
80
|
-
readonly body: string
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function parseGenerated(path: string, contents: string): ParsedFile | null {
|
|
84
|
-
let unwrap: HeaderSyntax['unwrap']
|
|
85
|
-
try {
|
|
86
|
-
unwrap = headerSyntax(path).unwrap
|
|
87
|
-
} catch {
|
|
88
|
-
return null
|
|
89
|
-
}
|
|
90
|
-
const lines = contents.split('\n')
|
|
91
|
-
|
|
92
|
-
if (unwrap(lines[0] ?? '') !== BANNER) return null
|
|
93
|
-
|
|
94
|
-
const sourceLine = unwrap(lines[1] ?? '')
|
|
95
|
-
const hashLine = unwrap(lines[3] ?? '')
|
|
96
|
-
if (sourceLine === null || !sourceLine.startsWith('Source: ')) return null
|
|
97
|
-
if (hashLine === null || !hashLine.startsWith(`${HASH_LABEL} `)) return null
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
source: sourceLine.slice('Source: '.length),
|
|
101
|
-
declaredHash: hashLine.slice(HASH_LABEL.length + 1),
|
|
102
|
-
body: lines.slice(HEADER_LINES).join('\n'),
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function lines(...parts: readonly (string | readonly string[])[]): string {
|
|
107
|
-
return parts.flat().join('\n').replace(/[ \t]+$/gm, '')
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function tsString(value: string): string {
|
|
111
|
-
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function stableJson(value: unknown, indent = 0): string {
|
|
115
|
-
const pad = ' '.repeat(indent)
|
|
116
|
-
const padInner = ' '.repeat(indent + 1)
|
|
117
|
-
|
|
118
|
-
if (value === null) return 'null'
|
|
119
|
-
if (Array.isArray(value)) {
|
|
120
|
-
if (value.length === 0) return '[]'
|
|
121
|
-
return `[\n${value.map((item) => `${padInner}${stableJson(item, indent + 1)}`).join(',\n')}\n${pad}]`
|
|
122
|
-
}
|
|
123
|
-
if (typeof value === 'object') {
|
|
124
|
-
const keys = Object.keys(value as Record<string, unknown>).sort()
|
|
125
|
-
if (keys.length === 0) return '{}'
|
|
126
|
-
return `{\n${keys
|
|
127
|
-
.map((key) => `${padInner}${JSON.stringify(key)}: ${stableJson((value as Record<string, unknown>)[key], indent + 1)}`)
|
|
128
|
-
.join(',\n')}\n${pad}}`
|
|
129
|
-
}
|
|
130
|
-
return JSON.stringify(value)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function pascal(id: string): string {
|
|
134
|
-
return id
|
|
135
|
-
.replace(/^(ent_|fld_|rel_|wf_|role_)/, '')
|
|
136
|
-
.split(/[_.]/)
|
|
137
|
-
.filter(Boolean)
|
|
138
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
139
|
-
.join('')
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function camel(id: string): string {
|
|
143
|
-
const p = pascal(id)
|
|
144
|
-
return p.charAt(0).toLowerCase() + p.slice(1)
|
|
145
|
-
}
|