@prestack/thread 0.1.20 → 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.
Files changed (148) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.d.ts +2 -0
  4. package/dist/core/descend.js +18 -3
  5. package/dist/core/generators/commit.js +55 -3
  6. package/dist/core/generators/dev.d.ts +1 -0
  7. package/dist/core/generators/dev.js +68 -0
  8. package/dist/core/rows.d.ts +5 -0
  9. package/dist/core/rows.js +45 -0
  10. package/dist/core/tree.d.ts +1 -0
  11. package/dist/core/tree.js +7 -1
  12. package/dist/derivation/generators/build.d.ts +3 -1
  13. package/dist/derivation/generators/build.js +103 -22
  14. package/dist/derivation/generators/release.js +74 -26
  15. package/dist/derivation/release.js +3 -3
  16. package/dist/integrations/cli.d.ts +3 -9
  17. package/dist/integrations/cli.js +96 -36
  18. package/dist/integrations/declared.d.ts +8 -0
  19. package/dist/integrations/declared.js +22 -0
  20. package/dist/integrations/pglite.js +13 -3
  21. package/dist/integrations/target.d.ts +4 -1
  22. package/dist/integrations/target.js +20 -6
  23. package/dist/integrity/attach.js +1 -5
  24. package/dist/integrity/generators/publish.js +2 -1
  25. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  26. package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
  27. package/dist/operations/access.js +3 -2
  28. package/dist/operations/capabilities.d.ts +2 -0
  29. package/dist/operations/capabilities.js +2 -0
  30. package/dist/operations/generators/capability.js +234 -24
  31. package/dist/operations/generators/compile.d.ts +21 -0
  32. package/dist/operations/generators/compile.js +151 -51
  33. package/dist/operations/generators/permits.d.ts +2 -1
  34. package/dist/operations/generators/permits.js +52 -47
  35. package/dist/operations/generators/seed.d.ts +3 -0
  36. package/dist/operations/generators/seed.js +25 -3
  37. package/dist/operations/nodes.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/core/actor.ts +0 -21
  40. package/dist/core/attach.ts +0 -613
  41. package/dist/core/chain.ts +0 -145
  42. package/dist/core/condition.ts +0 -227
  43. package/dist/core/descend.ts +0 -1173
  44. package/dist/core/errors/catalogue.ts +0 -314
  45. package/dist/core/errors/classes.ts +0 -51
  46. package/dist/core/errors/error.ts +0 -152
  47. package/dist/core/errors/namespaces.ts +0 -19
  48. package/dist/core/finding.ts +0 -37
  49. package/dist/core/generators/commit.ts +0 -157
  50. package/dist/core/init.ts +0 -222
  51. package/dist/core/invoked.ts +0 -52
  52. package/dist/core/round.ts +0 -74
  53. package/dist/core/rows.ts +0 -459
  54. package/dist/core/scope.ts +0 -20
  55. package/dist/core/thread.ts +0 -29
  56. package/dist/core/tree.ts +0 -90
  57. package/dist/core/types.ts +0 -351
  58. package/dist/derivation/attach.ts +0 -57
  59. package/dist/derivation/generators/build.ts +0 -371
  60. package/dist/derivation/generators/check.ts +0 -118
  61. package/dist/derivation/generators/determinism.ts +0 -145
  62. package/dist/derivation/generators/release.ts +0 -236
  63. package/dist/derivation/release.ts +0 -43
  64. package/dist/integrations/attach.ts +0 -187
  65. package/dist/integrations/cli.ts +0 -615
  66. package/dist/integrations/disk.ts +0 -466
  67. package/dist/integrations/generators/read.ts +0 -127
  68. package/dist/integrations/generators/vault.ts +0 -210
  69. package/dist/integrations/https.ts +0 -216
  70. package/dist/integrations/pglite.ts +0 -206
  71. package/dist/integrations/secret.ts +0 -54
  72. package/dist/integrations/target.ts +0 -656
  73. package/dist/integrations/vault.ts +0 -148
  74. package/dist/integrity/attach.ts +0 -150
  75. package/dist/integrity/check-threads.ts +0 -166
  76. package/dist/integrity/constraint.ts +0 -1246
  77. package/dist/integrity/generators/compiler.ts +0 -138
  78. package/dist/integrity/generators/gate.ts +0 -227
  79. package/dist/integrity/generators/issues.ts +0 -160
  80. package/dist/integrity/generators/publish.ts +0 -232
  81. package/dist/integrity/generators/trial.ts +0 -359
  82. package/dist/integrity/lint.ts +0 -214
  83. package/dist/integrity/lsp/bin.ts +0 -44
  84. package/dist/integrity/lsp/diagnostics.ts +0 -92
  85. package/dist/integrity/lsp/findings.ts +0 -63
  86. package/dist/integrity/lsp/lint.ts +0 -124
  87. package/dist/integrity/lsp/protocol.ts +0 -78
  88. package/dist/integrity/lsp/server.ts +0 -224
  89. package/dist/integrity/slopnator.ts +0 -324
  90. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  91. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  92. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  93. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  94. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  95. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  96. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  97. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  98. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  99. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  100. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  101. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  102. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  103. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  104. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  105. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  106. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  107. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  108. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  109. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  110. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  111. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  112. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  113. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  114. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  115. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  116. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  117. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  118. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  119. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  120. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  121. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  122. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  123. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  124. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  125. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  126. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  127. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  128. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  129. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  130. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  131. package/dist/integrity/sweep.ts +0 -395
  132. package/dist/knowledge/attach.ts +0 -327
  133. package/dist/knowledge/generators/docsgen.ts +0 -186
  134. package/dist/knowledge/generators/glossary.ts +0 -73
  135. package/dist/knowledge/glossary.ts +0 -6
  136. package/dist/operations/access.ts +0 -131
  137. package/dist/operations/attach.ts +0 -408
  138. package/dist/operations/capabilities.ts +0 -88
  139. package/dist/operations/dispatch.ts +0 -294
  140. package/dist/operations/error-results.ts +0 -31
  141. package/dist/operations/generators/capability.ts +0 -1054
  142. package/dist/operations/generators/compile.ts +0 -701
  143. package/dist/operations/generators/permits.ts +0 -224
  144. package/dist/operations/generators/seed.ts +0 -359
  145. package/dist/operations/generators/trial.ts +0 -666
  146. package/dist/operations/nodes.ts +0 -53
  147. package/dist/operations/operations.ts +0 -91
  148. package/dist/operations/result.ts +0 -28
@@ -29,4 +29,9 @@ export declare function withTopField(text: string, field: string, value: string)
29
29
  export declare function withoutField(text: string, where: Bounds, id: string, field: string): string;
30
30
  export declare function withoutRow(text: string, where: Bounds, id: string): string;
31
31
  export declare function marked(parts: readonly string[]): string;
32
+ export declare const markOf: (row: object) => string;
33
+ export declare function literalIn(text: string): unknown;
34
+ export declare const unreadable: (held: unknown) => boolean;
35
+ export declare function written(held: unknown, pad: string): string;
36
+ export declare function withWritten(text: string, where: Bounds, id: string, field: string, held: unknown): string | undefined;
32
37
  export declare function showing(row: object, only?: readonly string[]): string;
package/dist/core/rows.js CHANGED
@@ -432,6 +432,51 @@ const lines = (held, pad) => {
432
432
  export function marked(parts) {
433
433
  return createHash('sha256').update(parts.join('\n')).digest('hex').slice(0, 6);
434
434
  }
435
+ export const markOf = (row) => {
436
+ const held = row;
437
+ return marked([
438
+ typeof held.id === 'string' ? held.id : '',
439
+ typeof held.at === 'string' ? held.at : '',
440
+ typeof held.describes === 'string' ? held.describes : '',
441
+ held.version === undefined ? '' : String(held.version),
442
+ ]);
443
+ };
444
+ const WHOLE = /^-?[0-9][0-9_]*(?:\.[0-9]+)?$/;
445
+ export function literalIn(text) {
446
+ const said = text.trim();
447
+ if (WHOLE.test(said))
448
+ return Number(said.split('_').join(''));
449
+ const held = taken(`const held = ${said}`);
450
+ return held.length === 1 && held[0]?.[1] !== UNREAD ? held[0]?.[1] : UNREAD;
451
+ }
452
+ export const unreadable = (held) => held === UNREAD;
453
+ const BARE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
454
+ function inline(held) {
455
+ if (typeof held === 'string')
456
+ return quoted(held);
457
+ if (Array.isArray(held))
458
+ return `[${held.map(inline).join(', ')}]`;
459
+ if (held !== null && typeof held === 'object') {
460
+ const pairs = Object.entries(held).map(([key, one]) => `${BARE.test(key) ? key : quoted(key)}: ${inline(one)}`);
461
+ return pairs.length === 0 ? '{}' : `{ ${pairs.join(', ')} }`;
462
+ }
463
+ return String(held);
464
+ }
465
+ export function written(held, pad) {
466
+ if (!Array.isArray(held) || held.every((one) => typeof one !== 'object' || one === null))
467
+ return inline(held);
468
+ return ['[', ...held.map((one) => `${pad} ${inline(one)},`), `${pad}]`].join('\n');
469
+ }
470
+ export function withWritten(text, where, id, field, held) {
471
+ const row = rowAt(text, where, id);
472
+ if (row === undefined)
473
+ return undefined;
474
+ const named = row.properties.find((one) => ts.isPropertyAssignment(one) && keyed(one) === field) ?? row.properties.at(-1);
475
+ if (named === undefined)
476
+ return undefined;
477
+ const line = text.lastIndexOf('\n', named.getStart()) + 1;
478
+ return fieldOn(text, row, field, written(held, /^[ \t]*/.exec(text.slice(line))?.[0] ?? ''));
479
+ }
435
480
  export function showing(row, only = []) {
436
481
  const held = row;
437
482
  const keys = Object.keys(held);
@@ -11,5 +11,6 @@ export declare class UnthreadedRepositoryError extends Error {
11
11
  }
12
12
  export declare function folder(): string;
13
13
  export declare function findTree(from?: string): Tree;
14
+ export declare function declaresUi(tree: Tree): boolean;
14
15
  export declare function strayFiles(root: string, prefix: string, ends: string): readonly string[];
15
16
  export declare function inTree(tree: Tree, file: string): string;
package/dist/core/tree.js CHANGED
@@ -1,6 +1,7 @@
1
- import { FOLDER, attached, existsSync, readdirSync, statSync } from '../integrations/disk.js';
1
+ import { FOLDER, attached, existsSync, readFileSync, readdirSync, statSync } from '../integrations/disk.js';
2
2
  import { dirname, join, relative, resolve } from 'node:path';
3
3
  import { cwd, env } from 'node:process';
4
+ import { holdsRow, listBounds } from './rows.js';
4
5
  export { FOLDER };
5
6
  const NAMES = ['_threads', 'threads'];
6
7
  const ROOT = 'thread.ts';
@@ -44,6 +45,11 @@ export function findTree(from = folder()) {
44
45
  at = up;
45
46
  }
46
47
  }
48
+ export function declaresUi(tree) {
49
+ const held = readFileSync(tree.root, 'utf8');
50
+ const declares = listBounds(held, 'declares');
51
+ return declares !== undefined && holdsRow(held, declares, 'ui');
52
+ }
47
53
  export function strayFiles(root, prefix, ends) {
48
54
  const skip = new Set(['node_modules', '.git', '.next', 'dist', '_generated', 'coverage']);
49
55
  const found = [];
@@ -1,6 +1,8 @@
1
1
  import { type BuildOptions } from 'esbuild-wasm/esm/browser.js';
2
2
  import { type Pass } from '../../core/descend.ts';
3
- export declare function bundle(options: BuildOptions): Promise<string>;
3
+ export declare function bundle(options: BuildOptions & {
4
+ readonly keeping?: Set<string>;
5
+ }): Promise<string>;
4
6
  declare function wanted(pass: Pass): readonly {
5
7
  entry: string;
6
8
  out: string;
@@ -12,7 +12,7 @@ import { argv, cwd, stderr, stdout } from 'node:process';
12
12
  import { pathToFileURL } from 'node:url';
13
13
  import { build, initialize, stop } from 'esbuild-wasm/esm/browser.js';
14
14
  import { descend, reached } from '../../core/descend.js';
15
- import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from '../../integrations/disk.js';
15
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from '../../integrations/disk.js';
16
16
  import { exit, invoked } from '../../core/invoked.js';
17
17
  import { valuesIn } from '../../core/rows.js';
18
18
  import { findTree } from '../../core/tree.js';
@@ -83,35 +83,110 @@ function exported(held, sub, conditions) {
83
83
  }
84
84
  return undefined;
85
85
  }
86
- function packaged(specifier, from, conditions, top) {
87
- const name = specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
88
- const sub = `.${specifier.slice(name.length)}`;
86
+ function named(specifier) {
87
+ return specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
88
+ }
89
+ function rooted(name, from, top) {
89
90
  for (let at = from; inside(top, at); at = dirname(at)) {
90
91
  const root = join(at, 'node_modules', name);
91
- const manifest = join(root, 'package.json');
92
- if (filed(manifest)) {
93
- const held = JSON.parse(readFileSync(manifest, 'utf8'));
94
- if (held.exports !== undefined) {
95
- const found = exported(held.exports, sub, conditions);
96
- return found === undefined ? undefined : located(join(root, found));
97
- }
98
- const main = typeof held.main === 'string' ? held.main : typeof held.module === 'string' ? held.module : 'index.js';
99
- return located(join(root, sub === '.' ? main : sub));
100
- }
92
+ if (filed(join(root, 'package.json')))
93
+ return root;
101
94
  if (at === top)
102
95
  return undefined;
103
96
  }
104
97
  return undefined;
105
98
  }
106
- const folder = (outside, top) => ({
99
+ function packaged(specifier, from, conditions, top) {
100
+ const name = named(specifier);
101
+ const sub = `.${specifier.slice(name.length)}`;
102
+ const root = rooted(name, from, top);
103
+ if (root === undefined)
104
+ return undefined;
105
+ const held = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
106
+ if (held.exports !== undefined) {
107
+ const found = exported(held.exports, sub, conditions);
108
+ return found === undefined ? undefined : located(join(root, found));
109
+ }
110
+ const main = typeof held.main === 'string' ? held.main : typeof held.module === 'string' ? held.module : 'index.js';
111
+ return located(join(root, sub === '.' ? main : sub));
112
+ }
113
+ function binary(at) {
114
+ const bytes = readFileSync(at, 'latin1');
115
+ if (bytes.length < 4)
116
+ return false;
117
+ const byte = (i) => bytes.charCodeAt(i);
118
+ if (byte(0) === 0x7f && bytes.slice(1, 4) === 'ELF')
119
+ return true;
120
+ if (byte(0) === 0x4d && byte(1) === 0x5a)
121
+ return true;
122
+ if (bytes.slice(0, 4) === '\0asm')
123
+ return true;
124
+ if (byte(1) === 0xfa && byte(2) === 0xed && byte(3) === 0xfe && (byte(0) === 0xce || byte(0) === 0xcf))
125
+ return true;
126
+ if (byte(0) === 0xfe && byte(1) === 0xed && byte(2) === 0xfa && (byte(3) === 0xce || byte(3) === 0xcf))
127
+ return true;
128
+ if (byte(0) === 0xca && byte(1) === 0xfe && byte(2) === 0xba && (byte(3) === 0xbe || byte(3) === 0xbf))
129
+ return true;
130
+ return false;
131
+ }
132
+ function owned(root) {
133
+ const found = [];
134
+ const seen = new Set();
135
+ const walk = (at) => {
136
+ const real = realpathSync(at);
137
+ if (seen.has(real))
138
+ return;
139
+ seen.add(real);
140
+ for (const one of readdirSync(at)) {
141
+ if (one === 'node_modules')
142
+ continue;
143
+ const path = join(at, one);
144
+ const held = statSync(path, { throwIfNoEntry: false });
145
+ if (held?.isDirectory() === true)
146
+ walk(path);
147
+ else if (held?.isFile() === true)
148
+ found.push(path);
149
+ }
150
+ };
151
+ walk(root);
152
+ return found;
153
+ }
154
+ const staying = new Map();
155
+ function installed(root, top, visiting = new Set()) {
156
+ const cached = staying.get(root);
157
+ if (cached !== undefined)
158
+ return cached;
159
+ if (visiting.has(root))
160
+ return false;
161
+ visiting.add(root);
162
+ let decided = owned(root).some(binary);
163
+ if (!decided) {
164
+ const held = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
165
+ const names = Object.keys(held.optionalDependencies ?? {});
166
+ decided = names.some((one) => {
167
+ const found = rooted(one, root, top);
168
+ return found !== undefined && installed(found, top, visiting);
169
+ });
170
+ }
171
+ visiting.delete(root);
172
+ staying.set(root, decided);
173
+ return decided;
174
+ }
175
+ const folder = (outside, top, keeping) => ({
107
176
  name: 'thread-folder',
108
177
  setup(held) {
109
178
  held.onResolve({ filter: /.*/ }, (args) => {
110
179
  const bare = !args.path.startsWith('.') && !isAbsolute(args.path);
111
180
  if (args.path.startsWith('node:') || RUNTIME.has(args.path))
112
181
  return { path: args.path, external: true, sideEffects: false };
113
- if (bare && outside)
114
- return { path: args.path, external: true };
182
+ if (bare && outside) {
183
+ const name = named(args.path);
184
+ const root = rooted(name, args.resolveDir, top);
185
+ if (root !== undefined && installed(root, top)) {
186
+ keeping?.add(name);
187
+ return { path: args.path, external: true };
188
+ }
189
+ }
115
190
  const conditions = args.kind === 'require-call' || args.kind === 'require-resolve' ? REQUIRED : IMPORTED;
116
191
  const path = resolve(args.resolveDir, args.path);
117
192
  const found = bare ? packaged(args.path, args.resolveDir, conditions, top) : inside(top, path) ? located(path) : undefined;
@@ -141,13 +216,13 @@ export async function bundle(options) {
141
216
  hosted.self ??= globalThis;
142
217
  await initialize({ wasmModule: compiled, worker: false });
143
218
  try {
144
- const { packages, plugins, absWorkingDir = cwd(), ...rest } = options;
219
+ const { packages, plugins, keeping, absWorkingDir = cwd(), ...rest } = options;
145
220
  const made = await build({
146
221
  absWorkingDir,
147
222
  ...rest,
148
223
  bundle: true,
149
224
  write: false,
150
- plugins: [...(plugins ?? []), folder(packages === 'external', absWorkingDir)],
225
+ plugins: [...(plugins ?? []), folder(packages === 'external', absWorkingDir, keeping)],
151
226
  });
152
227
  const text = made.outputFiles?.[0]?.text;
153
228
  if (text === undefined)
@@ -323,13 +398,14 @@ async function main() {
323
398
  }
324
399
  const forBrowser = browsing(entry);
325
400
  const refusals = forBrowser ? withheld(pass) : new Map();
401
+ const keeping = forBrowser ? undefined : new Set();
326
402
  let held;
327
403
  try {
328
404
  held = await bundle({
329
405
  entryPoints: [resolve(ROOT, entry)],
330
406
  format: 'esm',
331
407
  platform: forBrowser ? 'browser' : 'node',
332
- ...(forBrowser ? {} : { packages: 'external' }),
408
+ ...(keeping === undefined ? {} : { packages: 'external', keeping }),
333
409
  outfile: at,
334
410
  plugins: [declared(), ...(refusals.size === 0 ? [] : [withholding(refusals)]), ...compiling],
335
411
  logLevel: 'silent',
@@ -339,16 +415,21 @@ async function main() {
339
415
  stderr.write(`build: ${out} was not written — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
340
416
  exit(1);
341
417
  }
418
+ const installs = keeping === undefined
419
+ ? ''
420
+ : keeping.size === 0
421
+ ? ' Every package it imports is compiled in, so npm installs nothing to run it.'
422
+ : ` It still needs npm to install ${String(keeping.size)} package${keeping.size === 1 ? '' : 's'}: ${[...keeping].sort().join(', ')}. Each one loads a native file or starts a program that only exists once npm installs it.`;
342
423
  if (checking) {
343
424
  if (held !== before) {
344
425
  stderr.write(`build: ${out} is not what ${BUILDS} writes. A built artifact has no author — run \`${BUILDS}\` and commit the result.\n`);
345
426
  exit(1);
346
427
  }
347
- stdout.write(`build: ${out} is up to date — ${String(new Set(reached()).size)} declared file(s) reachable.\n`);
428
+ stdout.write(`build: ${out} is up to date — ${String(new Set(reached()).size)} declared file(s) reachable.${installs}\n`);
348
429
  continue;
349
430
  }
350
431
  writeFileSync(at, held, 'utf8');
351
- stdout.write(`build: wrote ${out} from ${entry} — ${String(new Set(reached()).size)} declared file(s) reachable.\n`);
432
+ stdout.write(`build: wrote ${out} from ${entry} — ${String(new Set(reached()).size)} declared file(s) reachable.${installs}\n`);
352
433
  }
353
434
  }
354
435
  if (invoked(import.meta.url))
@@ -1,15 +1,20 @@
1
1
  import { join } from 'node:path';
2
2
  import { argv, stderr, stdout } from 'node:process';
3
- import { existsSync, readFileSync, writeFileSync } from '../../integrations/disk.js';
3
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from '../../integrations/disk.js';
4
+ import { COPY_AT, PROGRAM } from '../../integrations/cli.js';
5
+ import { VAULT_DIR } from '../../integrations/vault.js';
4
6
  import { exit, invoked } from '../../core/invoked.js';
5
- import { marked, valuesIn } from '../../core/rows.js';
7
+ import { markOf, valuesIn } from '../../core/rows.js';
6
8
  import { ours } from '../../core/scope.js';
7
- import { findTree, inTree } from '../../core/tree.js';
9
+ import { declaresUi, findTree, inTree } from '../../core/tree.js';
10
+ import { SERVER_OUT } from '../../operations/capabilities.js';
8
11
  import { CHECKED_BY, KINDS, LISTENS, SAID, WRITES, WRITTEN_BY, kindOf } from '../release.js';
9
12
  const TREE = findTree();
10
13
  const MANIFEST = 'package.json';
14
+ const RECEIPT = join(VAULT_DIR, 'gate.json');
15
+ const ROUTE = /^app\/(?:.+\/)?route\.ts$/;
11
16
  const addressed = (rows) => rows.flatMap((one) => [...(one.at === undefined ? [] : [one.at]), ...addressed(one.derives ?? [])]);
12
- const mark = (one) => marked([one.id ?? '', one.at ?? '']);
17
+ const mark = (one) => markOf(one);
13
18
  function releases() {
14
19
  const at = inTree(TREE, 'derivation.ts');
15
20
  if (!existsSync(at))
@@ -32,35 +37,73 @@ function manager() {
32
37
  return 'npm';
33
38
  }
34
39
  }
35
- function scripted() {
36
- const tool = manager();
37
- return { build: `${tool} run build`, start: tool === 'npm' ? 'npm start' : `${tool} start` };
40
+ function served() {
41
+ const found = new Set();
42
+ const walk = (held) => {
43
+ if (held === null || typeof held !== 'object')
44
+ return;
45
+ const row = held;
46
+ const routes = Array.isArray(row.derives) ? row.derives.filter((one) => ROUTE.test(one.at ?? '')) : [];
47
+ if (typeof row.at === 'string' && !row.at.startsWith(`${TREE.name}/`) && routes.length > 0)
48
+ found.add(row.at);
49
+ for (const one of Object.values(held))
50
+ walk(one);
51
+ };
52
+ for (const name of readdirSync(TREE.dir).filter((one) => one.endsWith('.ts')).sort()) {
53
+ walk(valuesIn(readFileSync(join(TREE.dir, name), 'utf8')));
54
+ }
55
+ return [...found].sort();
56
+ }
57
+ function runsThread() {
58
+ const at = inTree(TREE, 'targets.ts');
59
+ if (!existsSync(at))
60
+ return false;
61
+ const held = valuesIn(readFileSync(at, 'utf8'));
62
+ return (held.TARGETS ?? []).some((one) => one.id === PROGRAM);
38
63
  }
39
- function dockerfile(build, start) {
64
+ function dockerfile() {
40
65
  const tool = manager();
41
66
  const corepack = tool === 'npm' ? [] : ['RUN corepack enable'];
42
67
  const lock = tool === 'npm' ? 'package-lock.json*' : `${tool}-lock.yaml*`;
43
- const first = tool === 'npm'
44
- ? 'RUN npm ci --omit=dev --ignore-scripts || npm install --omit=dev --ignore-scripts'
45
- : `RUN ${tool} install --prod --frozen-lockfile --ignore-scripts || ${tool} install --prod --ignore-scripts`;
46
- const all = tool === 'npm' ? 'RUN npm install --ignore-scripts' : `RUN ${tool} install --ignore-scripts`;
68
+ const install = tool === 'npm'
69
+ ? 'RUN npm ci --ignore-scripts || npm install --ignore-scripts'
70
+ : `RUN ${tool} install --frozen-lockfile --ignore-scripts || ${tool} install --ignore-scripts`;
71
+ const thread = runsThread();
72
+ const carried = [RECEIPT, ...served(), ...(thread ? [COPY_AT] : [])];
73
+ const running = ['WORKDIR /app', 'ENV NODE_ENV=production', 'ENV HOSTNAME=0.0.0.0', `ENV PORT=${String(LISTENS)}`];
74
+ const ending = [...(thread ? [`RUN mkdir -p ${TREE.name}`] : []), `EXPOSE ${String(LISTENS)}`];
75
+ if (!declaresUi(TREE)) {
76
+ const production = tool === 'npm'
77
+ ? 'RUN npm ci --omit=dev --ignore-scripts || npm install --omit=dev --ignore-scripts'
78
+ : `RUN ${tool} install --prod --frozen-lockfile --ignore-scripts || ${tool} install --prod --ignore-scripts`;
79
+ return [
80
+ 'FROM node:22-slim',
81
+ ...running,
82
+ ...corepack,
83
+ `COPY package.json ${lock} ./`,
84
+ production,
85
+ ...[SERVER_OUT, ...carried].map((one) => `COPY ${one} ./${one}`),
86
+ ...ending,
87
+ `CMD ${JSON.stringify(['node', SERVER_OUT])}`,
88
+ '',
89
+ ].join('\n');
90
+ }
47
91
  return [
48
92
  'FROM node:22-slim AS build',
49
93
  'WORKDIR /app',
50
94
  ...corepack,
51
95
  `COPY package.json ${lock} ./`,
52
- first,
96
+ install,
53
97
  'COPY . .',
54
- all,
55
- `RUN ${build}`,
98
+ `RUN ${tool} run build`,
56
99
  '',
57
100
  'FROM node:22-slim',
58
- 'WORKDIR /app',
59
- ...corepack,
60
- 'ENV NODE_ENV=production',
61
- 'COPY --from=build /app ./',
62
- `EXPOSE ${String(LISTENS)}`,
63
- `CMD ${JSON.stringify(start.split(' '))}`,
101
+ ...running,
102
+ 'COPY --from=build /app/.next/standalone ./',
103
+ 'COPY --from=build /app/.next/static ./.next/static',
104
+ ...carried.map((one) => `COPY --from=build /app/${one} ./${one}`),
105
+ ...ending,
106
+ `CMD ${JSON.stringify(['node', 'server.js'])}`,
64
107
  '',
65
108
  ].join('\n');
66
109
  }
@@ -76,7 +119,10 @@ function compose(id) {
76
119
  }
77
120
  function withheld() {
78
121
  return [
79
- '.thread',
122
+ VAULT_DIR,
123
+ `!${RECEIPT}`,
124
+ ...(runsThread() ? [`!${COPY_AT}`] : []),
125
+ '.next',
80
126
  'node_modules',
81
127
  '.git',
82
128
  '',
@@ -85,7 +131,6 @@ function withheld() {
85
131
  function made() {
86
132
  const found = new Map();
87
133
  const rows = releases();
88
- const { build, start } = scripted();
89
134
  for (const one of rows) {
90
135
  const id = one.id;
91
136
  if (id === undefined)
@@ -94,7 +139,7 @@ function made() {
94
139
  if (kind === undefined)
95
140
  continue;
96
141
  if (kind === 'docker') {
97
- found.set('Dockerfile', dockerfile(build, start));
142
+ found.set('Dockerfile', dockerfile());
98
143
  found.set('compose.yaml', compose(id));
99
144
  found.set('.dockerignore', withheld());
100
145
  }
@@ -187,8 +232,11 @@ function run() {
187
232
  }
188
233
  for (const [at, text] of files)
189
234
  writeFileSync(join(TREE.at, at), text, 'utf8');
190
- stdout.write('release: wrote ' + [...files.keys()].join(', ') + ' — built and started by what this app\'s own manifest ' +
191
- 'declares, listening on ' + String(LISTENS) + '.\n' +
235
+ stdout.write('release: wrote ' + [...files.keys()].join(', ') + ' — ' +
236
+ (declaresUi(TREE)
237
+ ? 'built by the build this app\'s own manifest declares, and started by `node server.js` from what that build compiled'
238
+ : 'started by `node ' + SERVER_OUT + '`, the server `thread build` compiled into this tree, beside only the packages installed for production') +
239
+ ', listening on ' + String(LISTENS) + '.\n' +
192
240
  'release: no secret is named there. A secret is read from the sealed store at the moment it is used, and ' +
193
241
  'naming it here would be telling whoever deploys this to put a credential in the environment instead.\n');
194
242
  return 0;
@@ -4,7 +4,7 @@ export const WRITES = {
4
4
  docker: [
5
5
  {
6
6
  at: 'Dockerfile',
7
- describes: 'How this app becomes an image and what starts it inside one — the build and start commands its own manifest declares, run with the package manager that manifest names, and the port its server listens on.',
7
+ describes: 'How this app becomes an image, and what starts it inside one. An app that declares `ui` is built inside the image by the build command its own manifest declares, with the package manager that manifest names, and started with `node server.js` from what Next compiled. One that does not builds nothing there: it is started with Node on the server `thread build` compiled into this tree, beside only the packages its manifest installs for production. Either way the image keeps the files its routes read from disk, the receipt of the gate that passed, and the compiled thread command when this app runs one. Nothing of the framework\'s source goes in either way.',
8
8
  },
9
9
  {
10
10
  at: 'compose.yaml',
@@ -12,13 +12,13 @@ export const WRITES = {
12
12
  },
13
13
  {
14
14
  at: '.dockerignore',
15
- describes: 'What a build of this image must never carry out of this directory: the sealed store and the key that opens it, the installed packages, and the history. Without it the build copies everything beside it, and every sealed value goes into the image and into whoever pulls it.',
15
+ describes: 'What a build of this image must never carry out of this directory: the sealed store and the key that opens it, an earlier build, the installed packages, and the history. From the sealed store\'s folder it lets through only the receipt of the gate that passed and, when this app runs the thread command, the compiled copy of that command. Neither holds a secret. Without it the build copies everything beside it, and every sealed value goes into the image and into whoever pulls it.',
16
16
  },
17
17
  ],
18
18
  };
19
19
  export const LISTENS = 3000;
20
20
  export const SAID = {
21
- docker: 'A container image built from this app and the compose file that runs it. It is the one that needs no account and no network to be proven, so it is what an app can be taken end to end with before anybody has chosen where it really lives. What it holds is derived: the build and start commands this app already declares, the port its own server listens on, and the name of every secret it says it must be given — the names only, because a value is typed by a person and a row has never held one.',
21
+ docker: 'A container image built from this app and the compose file that runs it. It needs no account and no network to be proven, so an app can be taken end to end with it before anybody has chosen where it really lives. What it holds is derived: the server, on the port it listens on, with the files its routes read from disk, the receipt of the gate that passed, and the compiled thread command when this app runs one. An app that declares `ui` is served by what Next compiles, started with `node server.js`; one that does not is served by the server `thread build` compiled into this tree, started with Node beside only the packages its manifest installs for production. It names no secret, because a value is typed by a person and read from the sealed store where it is used.',
22
22
  };
23
23
  export const kindOf = (id) => KINDS.find((one) => id.startsWith(PREFIXES[one]));
24
24
  export const WRITTEN_BY = 'thread release';
@@ -1,4 +1,4 @@
1
- import { LOCKFILES, type Call, type Ran, type Running, type Target } from '../integrations/target.ts';
1
+ import { LOCKFILES, type Ran, type Running } from '../integrations/target.ts';
2
2
  export interface Starting {
3
3
  readonly at?: string;
4
4
  readonly shows?: boolean | 'errors' | 'tee';
@@ -19,20 +19,14 @@ export interface Held {
19
19
  }
20
20
  export declare function releases(): void;
21
21
  export declare function claims(root: string, runs: string): Held;
22
- export declare function programFor(name: string, at: string): string;
22
+ export declare function programFor(name: string, at: string): readonly string[];
23
23
  export declare const SIGNING = "THREAD_RECEIPTS_KEY";
24
24
  export declare function signing(): string;
25
25
  export declare const PROGRAM: string;
26
26
  export declare const COPIES: string;
27
27
  export declare const COPY_AT: string;
28
28
  export declare function running(name: string, subcommands: Readonly<Record<string, string>>, at: string): Running;
29
- export interface Expansion {
30
- readonly program: string;
31
- readonly target: Target;
32
- readonly calls: readonly Call[];
33
- }
34
- export declare function expansions(at: string): readonly Expansion[];
35
- export declare function calls(target: string, at: string): Running;
29
+ export { calls, expansions, type Expansion } from '../integrations/declared.ts';
36
30
  export { LOCKFILES };
37
31
  export declare const manager: (into: string) => string;
38
32
  export declare function adding(into: string): readonly [string, string];