@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.
Files changed (137) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.js +6 -3
  4. package/dist/core/generators/commit.js +55 -3
  5. package/dist/core/generators/dev.d.ts +1 -0
  6. package/dist/core/generators/dev.js +68 -0
  7. package/dist/core/tree.d.ts +1 -0
  8. package/dist/core/tree.js +7 -1
  9. package/dist/derivation/generators/build.d.ts +3 -1
  10. package/dist/derivation/generators/build.js +103 -22
  11. package/dist/derivation/generators/release.js +74 -26
  12. package/dist/derivation/release.js +3 -3
  13. package/dist/integrations/cli.js +69 -10
  14. package/dist/integrations/pglite.js +13 -3
  15. package/dist/integrations/target.d.ts +3 -0
  16. package/dist/integrity/attach.js +1 -5
  17. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  18. package/dist/operations/access.js +3 -2
  19. package/dist/operations/capabilities.d.ts +2 -0
  20. package/dist/operations/capabilities.js +2 -0
  21. package/dist/operations/generators/capability.js +234 -24
  22. package/dist/operations/generators/compile.js +150 -50
  23. package/dist/operations/generators/seed.d.ts +3 -0
  24. package/dist/operations/generators/seed.js +25 -3
  25. package/dist/operations/nodes.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/dist/core/actor.ts +0 -21
  28. package/dist/core/attach.ts +0 -613
  29. package/dist/core/chain.ts +0 -145
  30. package/dist/core/condition.ts +0 -227
  31. package/dist/core/descend.ts +0 -1185
  32. package/dist/core/errors/catalogue.ts +0 -314
  33. package/dist/core/errors/classes.ts +0 -51
  34. package/dist/core/errors/error.ts +0 -152
  35. package/dist/core/errors/namespaces.ts +0 -19
  36. package/dist/core/finding.ts +0 -37
  37. package/dist/core/generators/commit.ts +0 -157
  38. package/dist/core/init.ts +0 -222
  39. package/dist/core/invoked.ts +0 -52
  40. package/dist/core/round.ts +0 -74
  41. package/dist/core/rows.ts +0 -506
  42. package/dist/core/scope.ts +0 -20
  43. package/dist/core/thread.ts +0 -29
  44. package/dist/core/tree.ts +0 -90
  45. package/dist/core/types.ts +0 -351
  46. package/dist/derivation/attach.ts +0 -57
  47. package/dist/derivation/generators/build.ts +0 -371
  48. package/dist/derivation/generators/check.ts +0 -118
  49. package/dist/derivation/generators/determinism.ts +0 -145
  50. package/dist/derivation/generators/release.ts +0 -236
  51. package/dist/derivation/release.ts +0 -43
  52. package/dist/integrations/attach.ts +0 -187
  53. package/dist/integrations/cli.ts +0 -606
  54. package/dist/integrations/declared.ts +0 -34
  55. package/dist/integrations/disk.ts +0 -466
  56. package/dist/integrations/generators/read.ts +0 -127
  57. package/dist/integrations/generators/vault.ts +0 -210
  58. package/dist/integrations/https.ts +0 -216
  59. package/dist/integrations/pglite.ts +0 -206
  60. package/dist/integrations/secret.ts +0 -54
  61. package/dist/integrations/target.ts +0 -672
  62. package/dist/integrations/vault.ts +0 -148
  63. package/dist/integrity/attach.ts +0 -150
  64. package/dist/integrity/check-threads.ts +0 -166
  65. package/dist/integrity/constraint.ts +0 -1246
  66. package/dist/integrity/generators/compiler.ts +0 -138
  67. package/dist/integrity/generators/gate.ts +0 -227
  68. package/dist/integrity/generators/issues.ts +0 -160
  69. package/dist/integrity/generators/publish.ts +0 -233
  70. package/dist/integrity/generators/trial.ts +0 -359
  71. package/dist/integrity/lint.ts +0 -214
  72. package/dist/integrity/lsp/bin.ts +0 -44
  73. package/dist/integrity/lsp/diagnostics.ts +0 -92
  74. package/dist/integrity/lsp/findings.ts +0 -63
  75. package/dist/integrity/lsp/lint.ts +0 -124
  76. package/dist/integrity/lsp/protocol.ts +0 -78
  77. package/dist/integrity/lsp/server.ts +0 -224
  78. package/dist/integrity/slopnator.ts +0 -324
  79. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  80. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  81. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  82. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  83. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  84. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  85. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  86. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  87. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  88. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  89. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  90. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  91. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  92. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  93. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  94. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  95. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  96. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  97. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  98. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  99. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  100. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  101. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  102. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  103. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  104. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  105. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  106. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  107. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  108. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  109. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  110. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  111. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  112. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  113. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  114. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  115. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  116. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  117. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  118. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  119. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  120. package/dist/integrity/sweep.ts +0 -395
  121. package/dist/knowledge/attach.ts +0 -327
  122. package/dist/knowledge/generators/docsgen.ts +0 -186
  123. package/dist/knowledge/generators/glossary.ts +0 -73
  124. package/dist/knowledge/glossary.ts +0 -6
  125. package/dist/operations/access.ts +0 -131
  126. package/dist/operations/attach.ts +0 -408
  127. package/dist/operations/capabilities.ts +0 -88
  128. package/dist/operations/dispatch.ts +0 -294
  129. package/dist/operations/error-results.ts +0 -31
  130. package/dist/operations/generators/capability.ts +0 -1054
  131. package/dist/operations/generators/compile.ts +0 -701
  132. package/dist/operations/generators/permits.ts +0 -229
  133. package/dist/operations/generators/seed.ts +0 -359
  134. package/dist/operations/generators/trial.ts +0 -666
  135. package/dist/operations/nodes.ts +0 -53
  136. package/dist/operations/operations.ts +0 -91
  137. package/dist/operations/result.ts +0 -28
@@ -1,140 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from '../../integrations/disk.ts'
2
- import { join } from 'node:path'
3
-
4
- import type { Slopnator } from '../slopnator.ts'
5
- import { ORGANISATION, ours } from '../../core/scope.ts'
6
- import { findTree } from '../../core/tree.ts'
7
-
8
- const NODE = /export\s+const\s+[A-Za-z_$][\w$]*\s*:\s*Threaded\b/
9
-
10
- const SHAPES = [/declares:\s*\[/g, /export\s+const\s+declares\b[^\n]*?=\s*\[/g]
11
-
12
- const ID = /\bid:\s*'([^']+)'/g
13
- const CALLED = /(?:^|[\s,[(])[a-zA-Z_$][\w$]*\(\s*'([^']+)'/g
14
-
15
- const QUOTED = /"(?:[^"\\\n]|\\.)*"|`(?:[^`\\]|\\.)*`/g
16
-
17
- function closes(code: string, from: number): number {
18
- let depth = 0
19
- for (let at = from; at < code.length; at += 1) {
20
- if (code[at] === '[') depth += 1
21
- else if (code[at] === ']') {
22
- depth -= 1
23
- if (depth === 0) return at
24
- }
25
- }
26
- return -1
27
- }
28
-
29
- function shippedIn(text: string): ReadonlySet<string> {
30
- const code = text.replace(QUOTED, '""')
31
- if (!NODE.test(code)) return new Set()
32
- const found = new Set<string>()
33
- for (const shape of SHAPES) {
34
- for (const one of code.matchAll(shape)) {
35
- const opens = (one.index ?? 0) + one[0].length - 1
36
- const shut = closes(code, opens)
37
- if (shut === -1) continue
38
- const rows = code.slice(opens, shut)
39
- for (const held of rows.matchAll(ID)) found.add(held[1] ?? '')
40
- for (const held of rows.matchAll(CALLED)) found.add(held[1] ?? '')
41
- }
42
- }
43
- return found
44
- }
45
-
46
- function heldIn(text: string): ReadonlySet<string> {
47
- const code = text.replace(QUOTED, '""')
48
- const found = new Set<string>()
49
- for (const one of code.matchAll(ID)) found.add(one[1] ?? '')
50
- for (const one of code.matchAll(CALLED)) found.add(one[1] ?? '')
51
- return found
52
- }
53
-
54
- function filesIn(dir: string): readonly string[] {
55
- try {
56
- return readdirSync(dir).filter((one) => one.endsWith('.ts')).map((one) => join(dir, one))
57
- } catch {
58
- return []
59
- }
60
- }
61
-
62
- function installed(root: string): readonly { readonly name: string; readonly src: string }[] {
63
- const at = join(root, 'node_modules', ORGANISATION)
64
- const found: { name: string; src: string }[] = []
65
- for (const one of safely(at)) {
66
- const name = `${ORGANISATION}/${one}`
67
- if (!ours(name)) continue
68
- const src = join(at, one, 'dist')
69
- if (existsSync(src) && statSync(src).isDirectory()) found.push({ name, src })
70
- }
71
- return found
72
- }
73
-
74
- function safely(dir: string): readonly string[] {
75
- try {
76
- return readdirSync(dir)
77
- } catch {
78
- return []
79
- }
80
- }
81
-
82
- function read(path: string): string {
83
- try {
84
- return readFileSync(path, 'utf8')
85
- } catch {
86
- return ''
87
- }
88
- }
89
-
90
- export const slopnator: Slopnator = {
91
- checks: (root) => {
92
- const tree = findTree(root)
93
-
94
- const held = new Set<string>()
95
- for (const path of filesIn(tree.dir)) for (const one of heldIn(read(path))) held.add(one)
96
-
97
- const wired = new Set<string>()
98
- for (const path of filesIn(tree.dir)) {
99
- for (const one of read(path).matchAll(/node_modules\/(@[\w.-]+\/[\w.-]+)\//g)) wired.add(one[1] ?? '')
100
- }
101
-
102
- const found: string[] = []
103
- for (const pkg of installed(root)) {
104
- if (!wired.has(pkg.name)) continue
105
- for (const path of filesIn(pkg.src)) {
106
- const shipped = shippedIn(read(path))
107
- if (shipped.size === 0) continue
108
- for (const id of [...shipped].sort()) {
109
- if (held.has(id)) continue
110
- found.push(
111
- `${pkg.name} ships \`${id}\` and this tree does not hold it — run \`thread attach ${pkg.name}\` to put it back`,
112
- )
113
- }
114
- }
115
- }
116
- return found
117
- },
118
- proves: {
119
- wrong: [
120
- {
121
- files: [
122
- { at: '_threads/_probe_missing.ts', source: "import type { Threaded } from '../../core/thread.ts'\n\nexport const M: Threaded = {\n declares: [\n {\n at: 'node_modules/@prestack/thread-probe/dist/foo.ts',\n },\n ],\n}\n" },
123
- { at: 'node_modules/@prestack/thread-probe/dist/foo.ts', source: "export const PKG: Threaded = {\n declares: [\n {\n id: 'missing-thing',\n },\n ],\n}\n" },
124
- ],
125
- at: '_threads/_probe_missing.ts',
126
- },
127
- ],
128
- right: [
129
- {
130
- files: [
131
- { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
132
- { at: '_threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: '_threads/_probe_missing.ts',\n },\n ],\n}\n" },
133
- { at: '_threads/_probe_missing.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const M: Threaded = {\n declares: [\n {\n id: 'colour',\n },\n {\n id: 'missing-thing',\n },\n {\n at: 'node_modules/@prestack/thread-probe/dist/foo.ts',\n },\n ],\n}\n" },
134
- { at: 'node_modules/@prestack/thread-probe/dist/foo.ts', source: "export const PKG: Threaded = {\n declares: [\n {\n id: 'missing-thing',\n },\n ],\n}\n" },
135
- ],
136
- at: '_threads/_probe_missing.ts',
137
- },
138
- ],
139
- },
140
- }
@@ -1,48 +0,0 @@
1
- import type { Slopnator } from '../slopnator.ts'
2
- import { here } from './sl_unthreaded.ts'
3
-
4
- const BARREL_NAME = /(^|\/)index\.(ts|tsx|mts|cts|js|mjs|cjs)$/
5
- const CODE = /\.(ts|tsx|mts|cts|js|mjs|cjs)$/
6
-
7
- const SAYS = "can't do barrels lazy ass"
8
-
9
- function reExportsOnly(text: string): boolean {
10
- const lines = text
11
- .split('\n')
12
- .map((one) => one.trim())
13
- .filter((one) => one !== '')
14
-
15
- if (lines.length === 0) return false
16
- return lines.every((one) => one.startsWith('export *') || one.startsWith('export {') || one.startsWith('}') || one.startsWith('} from') || /^[A-Za-z_$][\w$]*,?$/.test(one) || /^type [A-Za-z_$][\w$]*,?$/.test(one))
17
- }
18
-
19
- export const slopnator: Slopnator = {
20
- holds: (_node, at) => {
21
- const path = here(at.filename)
22
- if (!CODE.test(path)) return false
23
-
24
- if (BARREL_NAME.test(path)) {
25
- return `${SAYS} — this file is named for the folder it sits in, so it exists to list what the folder holds, and the tree already says that`
26
- }
27
-
28
- if (reExportsOnly(at.text)) {
29
- return `${SAYS} — every line of this file is a re-export, so it holds no thing of its own and names nothing the tree has not already named`
30
- }
31
-
32
- return false
33
- },
34
- proves: {
35
- wrong: [
36
- {
37
- files: [{ at: 'packages/_probe_barrel/index.ts', source: "export const held = 1\n" }],
38
- at: 'packages/_probe_barrel/index.ts',
39
- },
40
- ],
41
- right: [
42
- {
43
- files: [{ at: 'packages/_probe_barrel/held.ts', source: "export const held = 1\n" }],
44
- at: 'packages/_probe_barrel/held.ts',
45
- },
46
- ],
47
- },
48
- }
@@ -1,100 +0,0 @@
1
- import { dirname, join, normalize } from 'node:path'
2
-
3
- import type { Slopnator } from '../slopnator.ts'
4
-
5
- import { ROOT, descend, read, reached } from '../../core/descend.ts'
6
- import { rowsIn } from './sl_unthreaded.ts'
7
-
8
- const DOOR = 'programs'
9
-
10
- const ROOTED = ROOT + '/'
11
-
12
- const slash = (one: string): string => one.split('\\').join('/')
13
-
14
- const bare = (one: string): string => one.replace(/\.[^./]+$/, '')
15
-
16
- function doorway(): string | undefined {
17
- descend()
18
-
19
- for (const at of reached()) {
20
- const text = read(at)
21
- if (text === undefined) continue
22
- for (const one of rowsIn(text)) {
23
- if (one.id === DOOR && one.at !== undefined) return one.at
24
- }
25
- }
26
-
27
- return undefined
28
- }
29
-
30
- interface Node {
31
- readonly type?: string
32
- readonly source?: { readonly type?: string; readonly value?: unknown }
33
- readonly callee?: { readonly type?: string; readonly name?: string }
34
- readonly arguments?: readonly { readonly type?: string; readonly value?: unknown }[]
35
- }
36
-
37
- const named = (held: unknown): string | undefined => {
38
- const one = held as { type?: string; value?: unknown } | undefined
39
- return one?.type === 'Literal' && typeof one.value === 'string' ? one.value : undefined
40
- }
41
-
42
- export const slopnator: Slopnator = {
43
- on: ['ImportDeclaration', 'ImportExpression', 'CallExpression'],
44
- holds: (node, at) => {
45
- const held = node as Node
46
- const from =
47
- held.type === 'ImportDeclaration' || held.type === 'ImportExpression'
48
- ? (named(held.source) ?? named(held.arguments?.[0]))
49
- : held.callee?.name === 'require'
50
- ? named(held.arguments?.[0])
51
- : undefined
52
-
53
- if (from === undefined) return false
54
-
55
- const door = doorway()
56
- if (door === undefined) return false
57
-
58
- const here = at.filename.startsWith(ROOTED) ? at.filename.slice(ROOTED.length) : at.filename
59
- const aimed = from.startsWith('.') ? slash(normalize(join(dirname(here), from))) : from
60
- if (bare(aimed) !== bare(door)) return false
61
-
62
- return (
63
- 'it imports ' + door + ', which is where every process this framework starts comes from. A check that can reach it can ' +
64
- 'start something, and whatever it starts can reach back into the enforcement that called it'
65
- )
66
- },
67
- proves: {
68
- wrong: [
69
- {
70
- files: [
71
- { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
72
- { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_programs.ts',\n },\n ],\n}\n" },
73
- { at: 'threads/_probe_programs.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const INTEGRATIONS: Threaded = {\n id: 'integrations',\n declares: [\n {\n id: 'programs',\n at: 'thread/integrations/cli.ts',\n },\n ],\n}\n" },
74
- { at: 'thread/integrity/slopnators/sl_probe_spawn.ts', source: "import { starts } from '../../integrations/cli.ts'\nexport const slopnator = { holds: () => false, proves: { wrong: ['a'], right: ['b'] } }\n" },
75
- ],
76
- at: 'thread/integrity/slopnators/sl_probe_spawn.ts',
77
- },
78
- ],
79
- right: [
80
- {
81
- files: [
82
- {
83
- at: 'thread/integrity/slopnators/sl_probe_spawn.ts',
84
- source: "import { dirname } from 'node:path'\nexport const slopnator = { holds: () => false, proves: { wrong: ['a'], right: ['b'] } }\n",
85
- },
86
- ],
87
- at: 'thread/integrity/slopnators/sl_probe_spawn.ts',
88
- },
89
- {
90
- files: [
91
- {
92
- at: 'thread/integrity/slopnators/sl_probe_spawn.ts',
93
- source: "export const slopnator = { holds: () => false, proves: { wrong: ['a'], right: ['b'] } }\n",
94
- },
95
- ],
96
- at: 'thread/integrity/slopnators/sl_probe_spawn.ts',
97
- },
98
- ],
99
- },
100
- }
@@ -1,43 +0,0 @@
1
- import type { Slopnator, Spoken } from '../slopnator.ts'
2
-
3
- const shebang = (text: string): string | undefined => (text.startsWith('#!') ? text.slice(2, text.indexOf('\n')) : undefined)
4
-
5
- const spoken = (text: string, all: readonly Spoken[]): readonly Spoken[] => {
6
- const line = shebang(text)
7
- return all.filter((one, index) => !(index === 0 && one.value === line))
8
- }
9
-
10
- export const slopnator: Slopnator = {
11
- on: ['Program'],
12
- holds: (_node, at) => {
13
- const all = spoken(at.text, at.comments())
14
- if (all.length === 0) return false
15
- const first = (all[0]?.value ?? '')
16
- .split('\n')
17
- .map((line) => line.trim().replace(/^\*+\s?/, ''))
18
- .find((line) => line !== '') ?? ''
19
- return `${String(all.length)} comment(s), the first saying "${first.slice(0, 60)}"`
20
- },
21
- fixes: (_node, at) => {
22
- const all = spoken(at.text, at.comments())
23
- if (all.length === 0) return undefined
24
- let out = at.text
25
- for (const one of [...all].sort((a, b) => b.range[0] - a.range[0])) {
26
- out = out.slice(0, one.range[0]) + out.slice(one.range[1])
27
- }
28
- return out.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n')
29
- },
30
- proves: {
31
- wrong: [
32
- 'const a = 1 // a line comment',
33
- '/* a block comment */\nconst a = 1',
34
- '/** JSDoc on the export */\nexport const a = 1',
35
- ],
36
- right: [
37
- 'const a = "// not a comment"',
38
- 'const url = "https://example.com/docs"',
39
- 'const a = 1',
40
- '#!/usr/bin/env node\nconst a = 1',
41
- ],
42
- },
43
- }
@@ -1,276 +0,0 @@
1
- import { dirname, join, normalize } from 'node:path'
2
-
3
- import { parser } from 'typescript-eslint'
4
-
5
- import { descend } from '../../core/descend.ts'
6
- import { PREFIXES } from '../../core/thread.ts'
7
-
8
- import type { Slopnator } from '../slopnator.ts'
9
- import { here, index, parses, unreadable } from './sl_unthreaded.ts'
10
-
11
- const RELATIVE = /^\.\.?\//
12
-
13
- const UNDER = 'operations/'
14
-
15
- const COMPILES = 'thread compile'
16
-
17
- const INVOKER = 'dispatch'
18
-
19
- const slash = (one: string): string => one.split('\\').join('/')
20
-
21
- interface Held {
22
- readonly type?: string
23
- readonly name?: string
24
- readonly value?: unknown
25
- readonly callee?: Held
26
- readonly source?: Held
27
- readonly arguments?: readonly Held[]
28
- readonly quasis?: readonly { readonly value?: { readonly cooked?: string; readonly raw?: string } }[]
29
- }
30
-
31
- const IMPORTED = 'ImportBinding'
32
-
33
- const TYPE_ONLY = 'type'
34
-
35
- const QUERIED = 'TSTypeQuery'
36
-
37
- let bodies: ReadonlySet<string> | undefined
38
-
39
- function operations(): ReadonlySet<string> {
40
- if (bodies !== undefined) return bodies
41
-
42
- const prefixed = Object.values(PREFIXES)
43
- const found = new Set<string>()
44
- for (const [at, threadings] of descend().threaded) {
45
- for (const one of threadings) {
46
- if (one.list !== 'derives') continue
47
- if (one.row.by !== COMPILES) continue
48
- if (!prefixed.some((each) => one.by.startsWith(each))) continue
49
- found.add(at)
50
- }
51
- }
52
- bodies = found
53
- return found
54
- }
55
-
56
- function isBody(path: string): boolean {
57
- const known = index().reached
58
- if (known.has(path)) return operations().has(path)
59
- if (!path.startsWith(UNDER)) return false
60
- const file = path.slice(UNDER.length)
61
- return !file.includes('/') && Object.values(PREFIXES).some((one) => file.startsWith(one))
62
- }
63
-
64
- let endings: ReadonlyMap<string, string> | undefined
65
-
66
- function spellings(): ReadonlyMap<string, string> {
67
- if (endings !== undefined) return endings
68
- const found = new Map<string, string>()
69
- for (const one of index().reached) {
70
- const dot = one.lastIndexOf('.')
71
- if (dot > one.lastIndexOf('/')) found.set(one.slice(0, dot), one)
72
- }
73
- endings = found
74
- return found
75
- }
76
-
77
- function reaches(said: string, from: string): boolean {
78
- if (said === '') return false
79
- const at = slash(normalize(RELATIVE.test(said) ? join(from, said) : said))
80
- if (isBody(at)) return true
81
- const whole = spellings().get(at)
82
- return whole !== undefined && isBody(whole)
83
- }
84
-
85
- function spelled(node: Held | undefined, said: string[]): void {
86
- if (node === undefined || node === null || typeof node !== 'object') return
87
- if (node.type === 'Literal' && typeof node.value === 'string') said.push(node.value)
88
- for (const one of node.quasis ?? []) {
89
- const held = one.value?.cooked ?? one.value?.raw
90
- if (typeof held === 'string') said.push(held)
91
- }
92
- for (const value of Object.values(node as Record<string, unknown>)) {
93
- if (Array.isArray(value)) for (const one of value) spelled(one as Held, said)
94
- else if (value !== null && typeof value === 'object') spelled(value as Held, said)
95
- }
96
- }
97
-
98
- interface Bound {
99
- readonly type?: string
100
- readonly node?: { readonly importKind?: string }
101
- readonly parent?: { readonly importKind?: string; readonly source?: { readonly value?: unknown } }
102
- }
103
-
104
- interface Named {
105
- readonly defs?: readonly Bound[]
106
- readonly references?: readonly { readonly identifier?: unknown; readonly isValueReference?: boolean }[]
107
- }
108
-
109
- interface Scoped {
110
- readonly scopes?: readonly { readonly variables?: readonly Named[] }[]
111
- }
112
-
113
- interface Parsed {
114
- readonly ast: unknown
115
- readonly scopeManager?: Scoped
116
- }
117
-
118
- function handled(scopes: Scoped | undefined, from: string): ReadonlySet<unknown> {
119
- const found = new Set<unknown>()
120
- for (const scope of scopes?.scopes ?? []) {
121
- for (const one of scope.variables ?? []) {
122
- const made = (one.defs ?? [])[0]
123
- if (made?.type !== IMPORTED) continue
124
- if (made.node?.importKind === TYPE_ONLY || made.parent?.importKind === TYPE_ONLY) continue
125
- const said = made.parent?.source?.value
126
- if (typeof said !== 'string' || !reaches(said, from)) continue
127
- for (const seen of one.references ?? []) {
128
- if (seen.isValueReference !== false && seen.identifier !== undefined) found.add(seen.identifier)
129
- }
130
- }
131
- }
132
- return found
133
- }
134
-
135
- function walked(
136
- node: unknown,
137
- up: Held | undefined,
138
- seen: (one: Held, parent: Held | undefined) => string | undefined,
139
- ): string | undefined {
140
- if (node === null || typeof node !== 'object') return undefined
141
- if (Array.isArray(node)) {
142
- for (const one of node) {
143
- const said = walked(one, up, seen)
144
- if (said !== undefined) return said
145
- }
146
- return undefined
147
- }
148
- const held = node as Held
149
- const real = typeof held.type === 'string'
150
- if (real) {
151
- const said = seen(held, up)
152
- if (said !== undefined) return said
153
- }
154
- for (const value of Object.values(node as Record<string, unknown>)) {
155
- const said = walked(value, real ? held : up, seen)
156
- if (said !== undefined) return said
157
- }
158
- return undefined
159
- }
160
-
161
- export const slopnator: Slopnator = {
162
- holds: (_node, at) => {
163
- const path = here(at.filename)
164
- if (unreadable(path, at.text) !== undefined) return false
165
- if (!parses(path)) return false
166
-
167
- const text = String(at.text)
168
- if (!text.includes(UNDER)) return false
169
-
170
- const from = dirname(path)
171
-
172
- let parsed: Parsed
173
- try {
174
- parsed = (parser.parseForESLint as (t: string, o: unknown) => Parsed)(text, {
175
- range: true,
176
- ecmaVersion: 'latest',
177
- sourceType: 'module',
178
- ecmaFeatures: { jsx: true },
179
- })
180
- } catch {
181
- return false
182
- }
183
-
184
- const mine = handled(parsed.scopeManager, from)
185
-
186
- return (
187
- walked(parsed.ast, undefined, (held, parent) => {
188
- const dynamic =
189
- held.type === 'ImportExpression'
190
- ? held.source
191
- : held.type === 'CallExpression' && held.callee?.type === 'Identifier' && held.callee.name === 'require'
192
- ? held.arguments?.[0]
193
- : undefined
194
-
195
- if (dynamic !== undefined) {
196
- const said: string[] = []
197
- spelled(dynamic, said)
198
- const reaching = said.find((one) => one.includes(UNDER) || reaches(one, from))
199
- if (reaching !== undefined) {
200
- return (
201
- `this reaches \`${reaching}\` at run time, which points at an operation. An operation is handed ` +
202
- `to \`${INVOKER}\`, and a file that goes looking for one is looking for a second way in`
203
- )
204
- }
205
- }
206
-
207
- if (held.type !== 'Identifier' || typeof held.name !== 'string' || !mine.has(held)) return undefined
208
- if (parent?.type === QUERIED) return undefined
209
- if (
210
- parent?.type === 'CallExpression' &&
211
- parent.callee?.type === 'Identifier' &&
212
- parent.callee.name === INVOKER &&
213
- (parent.arguments ?? []).includes(held)
214
- ) {
215
- return undefined
216
- }
217
-
218
- return (
219
- `\`${held.name}\` is an operation, and this does something with it other than hand it to ` +
220
- `\`${INVOKER}\`. Every gate that decides whether it may run is in \`${INVOKER}\` — naming it, ` +
221
- 'aliasing it, taking it apart or passing it on all reach past every one of them'
222
- )
223
- }) ?? false
224
- )
225
- },
226
- proves: {
227
- wrong: [
228
- "import { operation } from './operations/wf_send_invoice.ts'\n\nawait operation.run(input)",
229
- "import { operation } from './operations/wf_send_invoice.ts'\n\nconst { run } = operation\n\nawait run(opened, held)",
230
- "import { operation } from './operations/wf_send_invoice.ts'\n\nawait operation(input)",
231
- "import { operation } from './operations/wf_send_invoice.ts'\n\nawait somewhere(operation, input)",
232
- "import { operation } from './operations/wf_send_invoice.ts'\n\nconst one = operation\n\nawait one.run(input)",
233
- "import { operation } from './operations/wf_send_invoice.ts'\n\nconst { requires } = operation",
234
- "import { operation } from './operations/wf_send_invoice.ts'\n\nlet one\none = operation\nawait one.run(input)",
235
- "import { operation } from './operations/wf_send_invoice.ts'\n\nconst [one] = [operation]\n\nawait one.run(input)",
236
- "import { operation } from './operations/wf_send_invoice.ts'\n\nawait (0, operation).run(input)",
237
- "import { operation } from './operations/wf_send_invoice.ts'\n\nexport { operation }",
238
- "const held = await import('./operations/wf_send_invoice.ts')",
239
- "const held = require('../operations/wf_send_invoice.ts')",
240
- "const held = await import('../operations/' + name + '.ts')",
241
- 'const held = await import(`../operations/${name}.ts`)',
242
- ],
243
- right: [
244
- {
245
- files: [
246
- { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
247
- { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_operations.ts',\n },\n ],\n}\n" },
248
- { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_send_invoice',\n describes: 'sends an invoice',\n derives: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
249
- { at: 'probe.tsx', source: "import { dispatch } from './operations/_dispatch.ts'\n\nimport { operation } from './operations/wf_send_invoice.ts'\n\nawait dispatch(operation, input, actor, opened)\n" },
250
- ],
251
- at: 'probe.tsx',
252
- },
253
- "import type { Operation } from '../../operations/operations.ts'\n\nexport const held: Operation = one",
254
- {
255
- files: [
256
- { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
257
- { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_operations.ts',\n },\n ],\n}\n" },
258
- { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_send_invoice',\n describes: 'sends an invoice',\n derives: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
259
- { at: 'probe.tsx', source: "import { dispatch } from './operations/_dispatch.ts'\n\nawait dispatch(wf_send_invoice_body, input, actor, opened)\n" },
260
- ],
261
- at: 'probe.tsx',
262
- },
263
- {
264
- files: [
265
- { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
266
- { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_operations.ts',\n },\n ],\n}\n" },
267
- { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_send_invoice',\n describes: 'sends an invoice',\n derives: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
268
- { at: 'probe.tsx', source: "import { dispatch } from './operations/_dispatch.ts'\n\nimport { operation } from './operations/wf_send_invoice.ts'\n\nlet one: typeof operation\n\nawait dispatch(operation, input, actor, opened)\n" },
269
- ],
270
- at: 'probe.tsx',
271
- },
272
- "import * as steps from './operations/wf_send_invoice/steps.ts'\n\nexport const held = steps.reads_the_invoice",
273
- "import * as steps from './steps/wf_send_invoice.ts'\n\nexport const held = steps.reads_the_invoice",
274
- ],
275
- },
276
- }
@@ -1,66 +0,0 @@
1
- import { existsSync, readFileSync } from '../../integrations/disk.ts'
2
- import { dirname, resolve } from 'node:path'
3
-
4
- import type { Slopnator } from '../slopnator.ts'
5
- import { findTree } from '../../core/tree.ts'
6
-
7
- const ROOT = findTree().at
8
-
9
- const IMPORTS = /(?:^|\n)\s*import\s+(?!type\s)[^'"]*['"]([^'"]+)['"]/g
10
-
11
- type Node = {
12
- source?: { value?: unknown }
13
- importKind?: string
14
- specifiers?: readonly { importKind?: string }[]
15
- }
16
-
17
- const target = (from: string, spec: string): string | undefined =>
18
- spec.startsWith('.') ? resolve(dirname(from), spec) : undefined
19
-
20
- const erased = (node: Node): boolean =>
21
- node.importKind === 'type' ||
22
- ((node.specifiers ?? []).length > 0 && (node.specifiers ?? []).every((one) => one.importKind === 'type'))
23
-
24
- export const slopnator: Slopnator = {
25
- on: ['ImportDeclaration'],
26
- holds: (node, at) => {
27
- if (erased(node as Node)) return false
28
- const spec = (node as Node).source?.value
29
- if (typeof spec !== 'string') return false
30
-
31
- const here = resolve(ROOT, at.filename)
32
- const there = target(here, spec)
33
- if (there === undefined || !existsSync(there)) return false
34
-
35
- for (const found of readFileSync(there, 'utf8').matchAll(IMPORTS)) {
36
- const back = found[1] ?? ''
37
- if (target(there, back) === here) return `${spec} imports \`${back}\` straight back, so the two initialise each other`
38
- }
39
- return false
40
- },
41
- proves: {
42
- wrong: [
43
- {
44
- files: [
45
- { at: 'threads/_probe_one.ts', source: "import { two } from './_probe_two.ts'\nexport const one = two\n" },
46
- { at: 'threads/_probe_two.ts', source: "import { one } from './_probe_one.ts'\nexport const two = one\n" },
47
- ],
48
- at: 'threads/_probe_one.ts',
49
- },
50
- ],
51
- right: [
52
- "import type { Law } from '../../core/generators/meta.ts'",
53
- "import { SLUG, type Shaped } from '../../core/types.ts'",
54
- "import { join } from 'node:path'",
55
- "import type { Rule } from 'eslint'",
56
- "import { CONSTRAINTS } from '../constraint.ts'",
57
- {
58
- files: [
59
- { at: 'threads/_probe_three.ts', source: "import type { Four } from './_probe_four.ts'\nexport const three: Four = 1\n" },
60
- { at: 'threads/_probe_four.ts', source: "import { three } from './_probe_three.ts'\nexport type Four = number\nexport const four = three\n" },
61
- ],
62
- at: 'threads/_probe_three.ts',
63
- },
64
- ],
65
- },
66
- }