@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,481 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import { fileURLToPath } from 'node:url'
|
|
3
|
-
|
|
4
|
-
import ts from 'typescript'
|
|
5
|
-
|
|
6
|
-
import { ROOT, THREAD_AT, configured, descend, hosting, sources, under, type Pass } from '../../core/descend.ts'
|
|
7
|
-
import { packageOf } from '../../core/attach.ts'
|
|
8
|
-
import { PREFIXES } from '../../core/thread.ts'
|
|
9
|
-
import { HOLDS, OPENS, type Reach } from '../../integrations/target.ts'
|
|
10
|
-
|
|
11
|
-
import type { Slopnator, SlopnatorContext } from '../slopnator.ts'
|
|
12
|
-
|
|
13
|
-
const FROM_SOURCE = import.meta.url.endsWith('.ts')
|
|
14
|
-
|
|
15
|
-
const SHAPE = 'operations/operations'
|
|
16
|
-
|
|
17
|
-
const OPENED_AT = fileURLToPath(new URL('../../' + SHAPE + (FROM_SOURCE ? '.ts' : '.d.ts'), import.meta.url))
|
|
18
|
-
|
|
19
|
-
const FRAMEWORK_AT = fileURLToPath(new URL('../../', import.meta.url))
|
|
20
|
-
|
|
21
|
-
const FRAMEWORK = 'thread'
|
|
22
|
-
|
|
23
|
-
const STATED_AT = resolve(ROOT, FRAMEWORK, SHAPE + '.ts')
|
|
24
|
-
|
|
25
|
-
const SHAPES = [...new Set([STATED_AT, OPENED_AT])]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const HANDED = 'Opened'
|
|
29
|
-
|
|
30
|
-
const EVERY = ''
|
|
31
|
-
|
|
32
|
-
const IMPORTED = 'import '
|
|
33
|
-
|
|
34
|
-
const MADE = 'new '
|
|
35
|
-
|
|
36
|
-
const ANYWHERE = 'globalThis'
|
|
37
|
-
|
|
38
|
-
const REQUIRED = 'require'
|
|
39
|
-
|
|
40
|
-
const RUNTIME = 'node:'
|
|
41
|
-
|
|
42
|
-
const SOURCE = /[.](?:ts|tsx|mts|cts)$/
|
|
43
|
-
|
|
44
|
-
const ROOTED = ROOT + '/'
|
|
45
|
-
|
|
46
|
-
const here = (filename: string): string => (filename.startsWith(ROOTED) ? filename.slice(ROOTED.length) : filename)
|
|
47
|
-
|
|
48
|
-
const plain = (specifier: string): string => (specifier.startsWith(RUNTIME) ? specifier.slice(RUNTIME.length) : specifier)
|
|
49
|
-
|
|
50
|
-
interface Door {
|
|
51
|
-
readonly hand: string
|
|
52
|
-
readonly spelling: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const HANDS: Readonly<Record<string, string>> = Object.fromEntries(
|
|
56
|
-
(Object.keys(HOLDS) as Reach[]).map((one) => [
|
|
57
|
-
one,
|
|
58
|
-
HOLDS[one].takes === 'entities' ? 'store' : HOLDS[one].takes === 'calls' ? 'target' : one,
|
|
59
|
-
]),
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
function opening(): Readonly<Record<string, readonly string[]>> {
|
|
63
|
-
const found: Record<string, string[]> = {}
|
|
64
|
-
const add = (hand: string, spellings: readonly string[]): void => {
|
|
65
|
-
found[hand] = [...(found[hand] ?? []), ...spellings]
|
|
66
|
-
}
|
|
67
|
-
for (const [reach, spellings] of Object.entries(OPENS)) add(HANDS[reach] ?? reach, spellings)
|
|
68
|
-
return found
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface Doors {
|
|
72
|
-
readonly modules: ReadonlyMap<string, Door>
|
|
73
|
-
readonly members: ReadonlyMap<string, ReadonlyMap<string, Door>>
|
|
74
|
-
readonly globals: ReadonlyMap<string, Door>
|
|
75
|
-
readonly properties: ReadonlyMap<string, Door>
|
|
76
|
-
readonly made: ReadonlyMap<string, Door>
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function doors(): Doors {
|
|
80
|
-
const modules = new Map<string, Door>()
|
|
81
|
-
const members = new Map<string, Map<string, Door>>()
|
|
82
|
-
const globals = new Map<string, Door>()
|
|
83
|
-
const properties = new Map<string, Door>()
|
|
84
|
-
const made = new Map<string, Door>()
|
|
85
|
-
for (const [hand, spellings] of Object.entries(opening())) {
|
|
86
|
-
for (const spelling of spellings) {
|
|
87
|
-
const door = { hand, spelling }
|
|
88
|
-
if (spelling.startsWith(IMPORTED)) {
|
|
89
|
-
const rest = spelling.slice(IMPORTED.length)
|
|
90
|
-
const dot = rest.lastIndexOf('.')
|
|
91
|
-
if (dot === -1 || rest.slice(dot).includes('/')) {
|
|
92
|
-
modules.set(plain(rest), door)
|
|
93
|
-
continue
|
|
94
|
-
}
|
|
95
|
-
const specifier = plain(rest.slice(0, dot))
|
|
96
|
-
const held = members.get(specifier) ?? new Map<string, Door>()
|
|
97
|
-
held.set(rest.slice(dot + 1), door)
|
|
98
|
-
members.set(specifier, held)
|
|
99
|
-
continue
|
|
100
|
-
}
|
|
101
|
-
if (spelling.startsWith(MADE)) {
|
|
102
|
-
made.set(spelling.slice(MADE.length), door)
|
|
103
|
-
continue
|
|
104
|
-
}
|
|
105
|
-
if (spelling.includes('.')) properties.set(spelling, door)
|
|
106
|
-
else globals.set(spelling, door)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return { modules, members, globals, properties, made }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const DOORS = doors()
|
|
113
|
-
|
|
114
|
-
const INTEGRATION = 'Integration'
|
|
115
|
-
|
|
116
|
-
const NEEDS = 'needs'
|
|
117
|
-
|
|
118
|
-
function needed(pass: Pass): ReadonlyMap<string, Door> {
|
|
119
|
-
const found = new Map<string, Door>()
|
|
120
|
-
const rows = [...pass.stated.values(), ...[...pass.threaded.values()].flat().map((one) => one.row)]
|
|
121
|
-
for (const row of rows) {
|
|
122
|
-
if (row.type !== INTEGRATION) continue
|
|
123
|
-
const held = row.fields.get(NEEDS)
|
|
124
|
-
if (held === undefined || !ts.isObjectLiteralExpression(held)) continue
|
|
125
|
-
for (const one of held.properties) {
|
|
126
|
-
if (!ts.isPropertyAssignment(one)) continue
|
|
127
|
-
const name = ts.isStringLiteral(one.name) || ts.isIdentifier(one.name) ? one.name.text : undefined
|
|
128
|
-
if (name !== undefined) found.set(name, { hand: HANDS['sdk'] ?? 'sdk', spelling: IMPORTED + name })
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return found
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const moduleOf = (door: Door): string => door.spelling.slice(IMPORTED.length, door.spelling.lastIndexOf('.'))
|
|
135
|
-
|
|
136
|
-
const stands = (door: Door): string => ', which the ' + door.hand + ' this app is handed stands in for'
|
|
137
|
-
|
|
138
|
-
interface Built {
|
|
139
|
-
readonly of: Pass
|
|
140
|
-
readonly program: ts.Program
|
|
141
|
-
readonly checker: ts.TypeChecker
|
|
142
|
-
readonly hands: ReadonlyMap<ts.Symbol, string>
|
|
143
|
-
readonly packages: ReadonlyMap<string, Door>
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let built: Built | undefined
|
|
147
|
-
|
|
148
|
-
function argumentsOf(type: ts.Type, checker: ts.TypeChecker): readonly ts.Type[] {
|
|
149
|
-
const found: ts.Type[] = [...(type.aliasTypeArguments ?? [])]
|
|
150
|
-
if ((type.flags & ts.TypeFlags.Object) !== 0 && ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0) {
|
|
151
|
-
found.push(...checker.getTypeArguments(type as ts.TypeReference))
|
|
152
|
-
}
|
|
153
|
-
return found
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function handed(type: ts.Type, checker: ts.TypeChecker, hand: string, into: Map<ts.Symbol, string>, seen: Set<ts.Type>, own: (symbol: ts.Symbol) => boolean): void {
|
|
157
|
-
if (seen.has(type)) return
|
|
158
|
-
seen.add(type)
|
|
159
|
-
if (type.isUnionOrIntersection()) {
|
|
160
|
-
for (const one of type.types) handed(one, checker, hand, into, seen, own)
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
for (const one of argumentsOf(type, checker)) handed(one, checker, hand, into, seen, own)
|
|
164
|
-
const index = checker.getIndexTypeOfType(type, ts.IndexKind.String)
|
|
165
|
-
if (index !== undefined) handed(index, checker, hand, into, seen, own)
|
|
166
|
-
const symbol = type.getSymbol()
|
|
167
|
-
if (symbol === undefined || !own(symbol)) return
|
|
168
|
-
if ((symbol.flags & (ts.SymbolFlags.Interface | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Class)) === 0) return
|
|
169
|
-
if (!into.has(symbol)) into.set(symbol, hand)
|
|
170
|
-
}
|
|
171
|
-
function given(node: ts.TypeNode, checker: ts.TypeChecker, into: Map<ts.Symbol, string>, own: (symbol: ts.Symbol) => boolean): void {
|
|
172
|
-
const take = (one: ts.Node): void => {
|
|
173
|
-
if (ts.isTypeReferenceNode(one)) {
|
|
174
|
-
const named = ts.isIdentifier(one.typeName) ? one.typeName : one.typeName.right
|
|
175
|
-
const found = checker.getSymbolAtLocation(named)
|
|
176
|
-
const real = found !== undefined && (found.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(found) : found
|
|
177
|
-
const shapes = ts.SymbolFlags.Interface | ts.SymbolFlags.Class | ts.SymbolFlags.TypeAlias
|
|
178
|
-
if (real !== undefined && own(real) && (real.flags & shapes) !== 0 && !into.has(real)) into.set(real, EVERY)
|
|
179
|
-
}
|
|
180
|
-
ts.forEachChild(one, take)
|
|
181
|
-
}
|
|
182
|
-
const inside = (one: ts.Node): void => {
|
|
183
|
-
if (ts.isMappedTypeNode(one)) {
|
|
184
|
-
if (one.type !== undefined) take(one.type)
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
ts.forEachChild(one, inside)
|
|
188
|
-
}
|
|
189
|
-
inside(node)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function compiled(): Built {
|
|
193
|
-
const pass = descend()
|
|
194
|
-
if (built !== undefined && built.of === pass) return built
|
|
195
|
-
const options = configured().options
|
|
196
|
-
const roots = [THREAD_AT, ...sources()].filter((one) => SOURCE.test(one)).map((one) => resolve(ROOT, one))
|
|
197
|
-
const program = ts.createProgram([...new Set([...SHAPES, ...roots])], options, hosting(options))
|
|
198
|
-
const checker = program.getTypeChecker()
|
|
199
|
-
const hands = new Map<ts.Symbol, string>()
|
|
200
|
-
const own = (symbol: ts.Symbol): boolean =>
|
|
201
|
-
(symbol.getDeclarations() ?? []).some((one) => {
|
|
202
|
-
const from = one.getSourceFile()
|
|
203
|
-
if (from.fileName.startsWith(FRAMEWORK_AT)) return true
|
|
204
|
-
return !program.isSourceFileDefaultLibrary(from) && !program.isSourceFileFromExternalLibrary(from)
|
|
205
|
-
})
|
|
206
|
-
for (const statement of SHAPES.flatMap((one) => program.getSourceFile(one)?.statements ?? [])) {
|
|
207
|
-
if (!ts.isTypeAliasDeclaration(statement) || statement.name.text !== HANDED) continue
|
|
208
|
-
const alias = checker.getSymbolAtLocation(statement.name)
|
|
209
|
-
if (alias !== undefined) hands.set(alias, EVERY)
|
|
210
|
-
handed(checker.getTypeFromTypeNode(statement.type), checker, EVERY, hands, new Set(), own)
|
|
211
|
-
given(statement.type, checker, hands, own)
|
|
212
|
-
}
|
|
213
|
-
built = { of: pass, program, checker, hands, packages: needed(pass) }
|
|
214
|
-
return built
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const INTEGRATIONS_ID = 'integrations'
|
|
218
|
-
|
|
219
|
-
function integral(pass: Pass, path: string): boolean {
|
|
220
|
-
for (const [at, threadings] of pass.threaded) {
|
|
221
|
-
if (at !== path) continue
|
|
222
|
-
for (const one of threadings) {
|
|
223
|
-
if (one.by === INTEGRATIONS_ID || under(pass, one.by, INTEGRATIONS_ID)) return true
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return false
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function operational(pass: Pass, path: string): boolean {
|
|
230
|
-
const prefixes = Object.values(PREFIXES)
|
|
231
|
-
const owned = (one: string): boolean => prefixes.some((prefix) => one.startsWith(prefix))
|
|
232
|
-
if (path.split('/').some((part) => owned(part))) return true
|
|
233
|
-
for (const [at, threadings] of pass.threaded) {
|
|
234
|
-
if (at !== path) continue
|
|
235
|
-
for (const one of threadings) {
|
|
236
|
-
if (owned(one.by) || owned(String(one.row.id ?? ''))) return true
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return false
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function handOf(type: ts.Type, checker: ts.TypeChecker, hands: ReadonlyMap<ts.Symbol, string>, seen: Set<ts.Type>): string | undefined {
|
|
243
|
-
if (seen.has(type)) return undefined
|
|
244
|
-
seen.add(type)
|
|
245
|
-
const named = type.aliasSymbol === undefined ? undefined : hands.get(type.aliasSymbol)
|
|
246
|
-
if (named !== undefined) return named
|
|
247
|
-
if (type.isUnionOrIntersection()) {
|
|
248
|
-
for (const one of type.types) {
|
|
249
|
-
const found = handOf(one, checker, hands, seen)
|
|
250
|
-
if (found !== undefined) return found
|
|
251
|
-
}
|
|
252
|
-
return undefined
|
|
253
|
-
}
|
|
254
|
-
const symbol = type.getSymbol() ?? type.aliasSymbol
|
|
255
|
-
const own = symbol === undefined ? undefined : hands.get(symbol)
|
|
256
|
-
if (own !== undefined) return own
|
|
257
|
-
for (const one of argumentsOf(type, checker)) {
|
|
258
|
-
const found = handOf(one, checker, hands, seen)
|
|
259
|
-
if (found !== undefined) return found
|
|
260
|
-
}
|
|
261
|
-
return undefined
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function implemented(file: ts.SourceFile, built: Built): ReadonlySet<string> {
|
|
265
|
-
const found = new Set<string>()
|
|
266
|
-
const { checker, hands } = built
|
|
267
|
-
const held = checker.getSymbolAtLocation(file)
|
|
268
|
-
if (held === undefined) return found
|
|
269
|
-
const carries = (type: ts.Type, depth: number, seen: Set<ts.Type>): void => {
|
|
270
|
-
if (seen.has(type)) return
|
|
271
|
-
seen.add(type)
|
|
272
|
-
const hand = handOf(type, checker, hands, new Set())
|
|
273
|
-
if (hand !== undefined) found.add(hand)
|
|
274
|
-
if (depth === 0) return
|
|
275
|
-
for (const one of type.isUnionOrIntersection() ? type.types : [type]) {
|
|
276
|
-
for (const signature of one.getCallSignatures()) carries(signature.getReturnType(), depth - 1, seen)
|
|
277
|
-
for (const property of one.getProperties()) carries(checker.getTypeOfSymbolAtLocation(property, file), depth - 1, seen)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
for (const exported of checker.getExportsOfModule(held)) {
|
|
281
|
-
const real = (exported.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(exported) : exported
|
|
282
|
-
if ((real.flags & ts.SymbolFlags.Value) === 0) continue
|
|
283
|
-
carries(checker.getTypeOfSymbolAtLocation(real, file), 2, new Set())
|
|
284
|
-
}
|
|
285
|
-
return found
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export const DOORWAY = 'door'
|
|
289
|
-
|
|
290
|
-
export const HANDED_TO = 'hand'
|
|
291
|
-
|
|
292
|
-
interface Noted {
|
|
293
|
-
readonly line: number
|
|
294
|
-
readonly said: string
|
|
295
|
-
readonly kind: string
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function judged(path: string, built: Built, operates: boolean, wanted: string): readonly string[] {
|
|
299
|
-
const file = built.program.getSourceFile(resolve(ROOT, path))
|
|
300
|
-
if (file === undefined) return []
|
|
301
|
-
const { checker, hands } = built
|
|
302
|
-
let door: boolean | undefined
|
|
303
|
-
let allowed: ReadonlySet<string> | undefined
|
|
304
|
-
const may = (hand: string): boolean => {
|
|
305
|
-
if (door === undefined) door = integral(built.of, path)
|
|
306
|
-
if (allowed === undefined) allowed = implemented(file, built)
|
|
307
|
-
return allowed.has(EVERY) || allowed.has(hand) || door || (operates && hand === EVERY)
|
|
308
|
-
}
|
|
309
|
-
const found: Noted[] = []
|
|
310
|
-
const line = (node: ts.Node): number => file.getLineAndCharacterOfPosition(node.getStart(file)).line + 1
|
|
311
|
-
const note = (node: ts.Node, said: string, kind: string): void => {
|
|
312
|
-
found.push({ line: line(node), said: 'line ' + String(line(node)) + ': ' + said, kind })
|
|
313
|
-
}
|
|
314
|
-
const locals = new Map<string, Door>()
|
|
315
|
-
const spaces = new Map<string, ReadonlyMap<string, Door>>()
|
|
316
|
-
|
|
317
|
-
const global = (id: ts.Identifier): boolean => {
|
|
318
|
-
const symbol = checker.getSymbolAtLocation(id)
|
|
319
|
-
if (symbol === undefined) return true
|
|
320
|
-
const declared = symbol.getDeclarations() ?? []
|
|
321
|
-
return declared.every((one) => one.getSourceFile().isDeclarationFile)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const named = (node: ts.Node): boolean => {
|
|
325
|
-
const parent = node.parent as { readonly name?: ts.Node } | undefined
|
|
326
|
-
return parent !== undefined && parent.name === node
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const inType = (node: ts.Node): boolean => {
|
|
330
|
-
let up: ts.Node | undefined = node
|
|
331
|
-
while (up !== undefined) {
|
|
332
|
-
if (ts.isTypeQueryNode(up)) return true
|
|
333
|
-
up = up.parent
|
|
334
|
-
}
|
|
335
|
-
return false
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const reaching = (specifier: string, node: ts.Node): void => {
|
|
339
|
-
const door = DOORS.modules.get(plain(specifier)) ?? built.packages.get(packageOf(specifier))
|
|
340
|
-
if (door !== undefined && !may(door.hand)) note(node, 'imports ' + specifier + stands(door), DOORWAY)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const visit = (node: ts.Node): void => {
|
|
344
|
-
if (ts.isImportDeclaration(node) && node.importClause?.isTypeOnly !== true && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
345
|
-
reaching(node.moduleSpecifier.text, node)
|
|
346
|
-
const held = DOORS.members.get(plain(node.moduleSpecifier.text))
|
|
347
|
-
const bindings = node.importClause?.namedBindings
|
|
348
|
-
if (held !== undefined && bindings !== undefined) {
|
|
349
|
-
if (ts.isNamespaceImport(bindings)) spaces.set(bindings.name.text, held)
|
|
350
|
-
else {
|
|
351
|
-
for (const element of bindings.elements) {
|
|
352
|
-
const door = held.get((element.propertyName ?? element.name).text)
|
|
353
|
-
if (door !== undefined) locals.set(element.name.text, door)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
if (ts.isExportDeclaration(node) && !node.isTypeOnly && node.moduleSpecifier !== undefined && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
359
|
-
reaching(node.moduleSpecifier.text, node)
|
|
360
|
-
}
|
|
361
|
-
if (ts.isCallExpression(node)) {
|
|
362
|
-
const first = node.arguments[0]
|
|
363
|
-
const loads = node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === REQUIRED)
|
|
364
|
-
if (loads && first !== undefined && ts.isStringLiteral(first)) reaching(first.text, node)
|
|
365
|
-
const callee = node.expression
|
|
366
|
-
if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) {
|
|
367
|
-
const taken = callee.expression
|
|
368
|
-
const hand =
|
|
369
|
-
handOf(checker.getTypeAtLocation(taken), checker, hands, new Set()) ??
|
|
370
|
-
(ts.isPropertyAccessExpression(taken) || ts.isElementAccessExpression(taken)
|
|
371
|
-
? handOf(checker.getTypeAtLocation(taken.expression), checker, hands, new Set())
|
|
372
|
-
: undefined)
|
|
373
|
-
if (hand !== undefined && !may(hand)) {
|
|
374
|
-
const what = ts.isPropertyAccessExpression(callee) ? callee.name.text : 'an order'
|
|
375
|
-
note(node, 'calls ' + what + ' on ' + (hand === EVERY ? 'what this app hands an operation' : 'the ' + hand + ' it is handed'), HANDED_TO)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && (node.arguments ?? []).length === 0) {
|
|
380
|
-
const door = DOORS.made.get(node.expression.text)
|
|
381
|
-
if (door !== undefined && !may(door.hand) && global(node.expression)) note(node, 'reaches ' + door.spelling + '()' + stands(door), DOORWAY)
|
|
382
|
-
}
|
|
383
|
-
if (ts.isPropertyAccessExpression(node) && !ts.isPartOfTypeNode(node)) {
|
|
384
|
-
const inner = ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === ANYWHERE ? node.expression.name : undefined
|
|
385
|
-
const object = inner ?? node.expression
|
|
386
|
-
if (ts.isIdentifier(object)) {
|
|
387
|
-
const spaced = spaces.get(object.text)?.get(node.name.text)
|
|
388
|
-
if (spaced !== undefined && !may(spaced.hand)) note(node, 'reaches ' + node.name.text + ' from ' + moduleOf(spaced) + stands(spaced), DOORWAY)
|
|
389
|
-
const door = DOORS.properties.get(object.text + '.' + node.name.text)
|
|
390
|
-
if (door !== undefined && !may(door.hand) && (inner !== undefined || global(object))) note(node, 'reaches ' + door.spelling + stands(door), DOORWAY)
|
|
391
|
-
if (object.text === ANYWHERE) {
|
|
392
|
-
const bare = DOORS.globals.get(node.name.text)
|
|
393
|
-
if (bare !== undefined && !may(bare.hand)) note(node, 'reaches ' + bare.spelling + stands(bare), DOORWAY)
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (ts.isIdentifier(node) && !named(node) && !ts.isPartOfTypeNode(node) && !inType(node)) {
|
|
398
|
-
const local = locals.get(node.text)
|
|
399
|
-
if (local !== undefined && !may(local.hand)) note(node, 'reaches ' + node.text + ' from ' + moduleOf(local) + stands(local), DOORWAY)
|
|
400
|
-
const door = DOORS.globals.get(node.text)
|
|
401
|
-
if (door !== undefined && !may(door.hand) && global(node)) note(node, 'reaches ' + door.spelling + stands(door), DOORWAY)
|
|
402
|
-
}
|
|
403
|
-
ts.forEachChild(node, visit)
|
|
404
|
-
}
|
|
405
|
-
visit(file)
|
|
406
|
-
return found.filter((one) => one.kind === wanted).sort((one, two) => one.line - two.line).map((one) => one.said)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export function judging(at: SlopnatorContext, wanted: string, closing: string): string | false {
|
|
410
|
-
const path = here(at.filename)
|
|
411
|
-
if (!SOURCE.test(path)) return false
|
|
412
|
-
const held = compiled()
|
|
413
|
-
if (held.hands.size === 0 && SHAPES.includes(resolve(ROOT, path))) {
|
|
414
|
-
return 'no type called ' + HANDED + ' can be read here, so nothing can tell what an operation was opened with from any other value, and a call on it outside an operation goes unmeasured'
|
|
415
|
-
}
|
|
416
|
-
const found = judged(path, held, operational(held.of, path), wanted)
|
|
417
|
-
if (found.length === 0) return false
|
|
418
|
-
return found.join('; ') + '. ' + closing
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
export const slopnator: Slopnator = {
|
|
422
|
-
holds: (_node, at) =>
|
|
423
|
-
judging(at, DOORWAY, 'Each of these reaches past this process without going through a declared integration, and a reach this app never declared is one nothing can price, replay or unwind.'),
|
|
424
|
-
proves: {
|
|
425
|
-
wrong: [
|
|
426
|
-
"import { spawnSync } from 'node:child_process'\n\nexport const ran = spawnSync('git', ['status'])",
|
|
427
|
-
"import * as processes from 'node:child_process'\n\nexport const ran = processes.spawnSync('git', ['status'])",
|
|
428
|
-
{
|
|
429
|
-
files: [
|
|
430
|
-
{ at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'wf_thing', at: 'operations/wf_thing.ts' },\n ],\n}\n" },
|
|
431
|
-
{ at: 'operations/wf_thing.ts', source: "export const asked = await fetch('https://example.com')" },
|
|
432
|
-
],
|
|
433
|
-
at: 'operations/wf_thing.ts',
|
|
434
|
-
},
|
|
435
|
-
"import { PGlite } from '@electric-sql/pglite'\n\nexport const db = new PGlite()",
|
|
436
|
-
{
|
|
437
|
-
files: [
|
|
438
|
-
{ 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}\n' },
|
|
439
|
-
{ at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport interface Integration extends Threaded {\n readonly needs?: Readonly<Record<string, string>>\n}\n\nexport const THREAD: Threaded = {\n declares: [\n { id: 'wf_pad', at: 'operations/wf_pad.ts' },\n ],\n}\n\nexport const INTEGRATIONS: readonly Integration[] = [\n { id: 'in_pad', describes: 'Pads text.', needs: { 'left-pad': '^1.3.0' } },\n]\n" },
|
|
440
|
-
{ at: 'operations/wf_pad.ts', source: "import pad from 'left-pad'\n\nexport const padded = pad('a', 3)\n" },
|
|
441
|
-
],
|
|
442
|
-
at: 'operations/wf_pad.ts',
|
|
443
|
-
},
|
|
444
|
-
],
|
|
445
|
-
right: [
|
|
446
|
-
'export const began = Date.now()',
|
|
447
|
-
'export const later = setTimeout(() => again(), 5000)',
|
|
448
|
-
"import { randomBytes } from 'node:crypto'\n\nexport const nonce = randomBytes(6).toString('hex')",
|
|
449
|
-
"import { join } from 'node:path'\n\nexport const at = join('a', 'b')",
|
|
450
|
-
"import { createHash } from 'node:crypto'\n\nexport const sum = createHash('sha256').update('a').digest('hex')",
|
|
451
|
-
'export const when = new Date(1767225600000)',
|
|
452
|
-
{
|
|
453
|
-
files: [
|
|
454
|
-
{ at: 'thread/operations/operations.ts', source: 'export interface Http {\n call(address: string): Promise<void>\n}\n\nexport type Opened<R extends string = never> = { readonly [K in R]: Http }\n' },
|
|
455
|
-
{ at: 'threads/_probe_door.ts', source: "import type { Http } from '../thread/operations/operations.ts'\n\nexport const made: Http = {\n call: async (address) => {\n await fetch(address)\n },\n}\n" },
|
|
456
|
-
],
|
|
457
|
-
at: 'threads/_probe_door.ts',
|
|
458
|
-
},
|
|
459
|
-
{
|
|
460
|
-
files: [
|
|
461
|
-
{ at: 'thread/operations/operations.ts', source: 'export interface Store {\n insert(row: unknown): Promise<void>\n}\n\nexport type Opened<R extends string = never> = { readonly [K in R]: Store }\n' },
|
|
462
|
-
{ at: 'threads/_probe_handed.ts', source: "import type { Opened } from '../thread/operations/operations.ts'\n\nexport const carried = (opened: Opened<'pg_main'>, run: (given: Opened<'pg_main'>) => Promise<void>): Promise<void> => run(opened)\n" },
|
|
463
|
-
],
|
|
464
|
-
at: 'threads/_probe_handed.ts',
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
files: [
|
|
468
|
-
{ 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}\n' },
|
|
469
|
-
{ at: 'thread/operations/operations.ts', source: 'export type Calling = { readonly [call: string]: (given?: unknown) => Promise<unknown> }\n\nexport type Opened<R extends string = never> = { readonly [K in R]: Calling }\n' },
|
|
470
|
-
{ at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport interface Integration extends Threaded {\n readonly needs?: Readonly<Record<string, string>>\n}\n\nexport const THREAD: Threaded = {\n declares: [\n { id: 'sdk_pad_calls', at: 'src/sdk/sdk_pad.ts' },\n ],\n}\n\nexport const INTEGRATIONS: readonly Integration[] = [\n { id: 'in_pad', describes: 'Pads text.', needs: { 'left-pad': '^1.3.0' } },\n]\n" },
|
|
471
|
-
{ at: 'src/sdk/sdk_pad.ts', source: "import type { Calling } from '../../thread/operations/operations.ts'\nimport pad from 'left-pad'\n\nexport const calls: Calling = {\n padded: async (given) => pad(String(given), 3),\n}\n" },
|
|
472
|
-
],
|
|
473
|
-
at: 'src/sdk/sdk_pad.ts',
|
|
474
|
-
},
|
|
475
|
-
],
|
|
476
|
-
},
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { Slopnator } from '../slopnator.ts'
|
|
2
|
-
|
|
3
|
-
import { descend, read, reached } from '../../core/descend.ts'
|
|
4
|
-
import { rowsIn } from './sl_unthreaded.ts'
|
|
5
|
-
|
|
6
|
-
interface Held {
|
|
7
|
-
readonly type?: string
|
|
8
|
-
readonly name?: unknown
|
|
9
|
-
readonly value?: unknown
|
|
10
|
-
readonly computed?: boolean
|
|
11
|
-
readonly object?: Held
|
|
12
|
-
readonly property?: Held
|
|
13
|
-
readonly key?: Held
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const HREF = 'href'
|
|
17
|
-
|
|
18
|
-
const named = (held: Held | undefined): string | undefined => {
|
|
19
|
-
if (held?.type === 'Identifier' || held?.type === 'JSXIdentifier') return typeof held.name === 'string' ? held.name : undefined
|
|
20
|
-
if (held?.type === 'Literal' && typeof held.value === 'string') return held.value
|
|
21
|
-
return undefined
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const spoken = (held: Held): string | undefined => {
|
|
25
|
-
if (held.type === 'JSXAttribute') return named(held.name as Held | undefined)
|
|
26
|
-
if (held.type === 'Property') return held.computed === true ? undefined : named(held.key)
|
|
27
|
-
return undefined
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const SCREEN = 'screen'
|
|
31
|
-
|
|
32
|
-
const PASCAL = (id: string): string =>
|
|
33
|
-
id
|
|
34
|
-
.split(/[-_]/)
|
|
35
|
-
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
36
|
-
.join('')
|
|
37
|
-
|
|
38
|
-
const nameOf = (at: string): string =>
|
|
39
|
-
PASCAL((at.split('\\').join('/').split('/').at(-1) ?? at).replace(/^scr_/, '').replace(/\.[^.]+$/, ''))
|
|
40
|
-
|
|
41
|
-
const routeFor = (id: string): string => `/${id}`
|
|
42
|
-
|
|
43
|
-
function addressed(): ReadonlyMap<string, string> {
|
|
44
|
-
const found = new Map<string, string>()
|
|
45
|
-
|
|
46
|
-
descend()
|
|
47
|
-
|
|
48
|
-
for (const at of reached()) {
|
|
49
|
-
const text = read(at)
|
|
50
|
-
if (text === undefined) continue
|
|
51
|
-
|
|
52
|
-
let waiting: { readonly id: string; readonly at: string }[] = []
|
|
53
|
-
for (const one of rowsIn(text)) {
|
|
54
|
-
if (one.id === undefined) continue
|
|
55
|
-
if (one.list === 'declares') {
|
|
56
|
-
if (one.at !== undefined) waiting.push({ id: one.id, at: one.at })
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
if (one.list !== undefined) continue
|
|
60
|
-
if (one.id === SCREEN) for (const row of waiting) found.set(routeFor(row.id), nameOf(row.at))
|
|
61
|
-
waiting = []
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return found
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const slopnator: Slopnator = {
|
|
69
|
-
on: ['JSXAttribute', 'Property', 'Literal'],
|
|
70
|
-
holds: (node) => {
|
|
71
|
-
const held = node as Held
|
|
72
|
-
|
|
73
|
-
if (spoken(held) === HREF) {
|
|
74
|
-
return (
|
|
75
|
-
'`href` moves somebody without naming a screen, so the map holds no link for it — ' +
|
|
76
|
-
'the destination reads as unreachable to the orphan pass, the app reads as having no path to it, and it navigates correctly'
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (held.type !== 'Literal' || typeof held.value !== 'string') return false
|
|
81
|
-
|
|
82
|
-
const screen = addressed().get(held.value)
|
|
83
|
-
if (screen === undefined) return false
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
'this writes the address of ' + screen + ' rather than naming it, so the map holds no link for it — ' +
|
|
87
|
-
'the destination reads as unreachable to the orphan pass and the app navigates there anyway. Name the screen: navigateTo="' + screen + '"'
|
|
88
|
-
)
|
|
89
|
-
},
|
|
90
|
-
proves: {
|
|
91
|
-
wrong: [
|
|
92
|
-
"const link = { href: '/orders/42' }",
|
|
93
|
-
'<a href="/orders/42">{label}</a>',
|
|
94
|
-
{
|
|
95
|
-
files: [
|
|
96
|
-
{ 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" },
|
|
97
|
-
{ 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_screens.ts',\n },\n ],\n}\n" },
|
|
98
|
-
{ at: 'threads/_probe_screens.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const SCREENS: Threaded = {\n id: 'screen',\n declares: [\n {\n id: 'orders',\n at: 'screens/scr_orders.tsx',\n },\n ],\n}\n" },
|
|
99
|
-
{ at: 'app/_probe_address.tsx', source: "export const HELD = router.push('/orders')\n" },
|
|
100
|
-
],
|
|
101
|
-
at: 'app/_probe_address.tsx',
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
right: [
|
|
105
|
-
'const path = router.pathname',
|
|
106
|
-
'<Link.linkInline navigateTo="Home" labelKey="home.title" />',
|
|
107
|
-
"const to = { navigateTo: 'Order', labelKey: 'order.title' }",
|
|
108
|
-
'const rows = orders.push(one)',
|
|
109
|
-
"const parts = one.split('/')",
|
|
110
|
-
"const at = '/api/webhook'",
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { Slopnator } from '../slopnator.ts'
|
|
2
|
-
|
|
3
|
-
interface Tag {
|
|
4
|
-
readonly type?: string
|
|
5
|
-
readonly name?: unknown
|
|
6
|
-
readonly namespace?: { readonly name?: unknown }
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const HOST = /^[a-z]/
|
|
10
|
-
|
|
11
|
-
function tagOf(node: unknown): string | undefined {
|
|
12
|
-
const name = (node as { name?: Tag } | undefined)?.name
|
|
13
|
-
if (name?.type === 'JSXNamespacedName') {
|
|
14
|
-
const space = typeof name.namespace?.name === 'string' ? name.namespace.name : ''
|
|
15
|
-
const local = typeof (name.name as Tag | undefined)?.name === 'string' ? String((name.name as Tag).name) : ''
|
|
16
|
-
return `${space}:${local}`
|
|
17
|
-
}
|
|
18
|
-
if (name?.type !== 'JSXIdentifier') return undefined
|
|
19
|
-
return typeof name.name === 'string' ? name.name : undefined
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const slopnator: Slopnator = {
|
|
23
|
-
on: ['JSXOpeningElement'],
|
|
24
|
-
holds: (node) => {
|
|
25
|
-
const tag = tagOf(node)
|
|
26
|
-
if (tag === undefined || !HOST.test(tag)) return false
|
|
27
|
-
return (
|
|
28
|
-
`<${tag}> is a host element, so it wears no declared style, sits in no catalogue, and the style guide, ` +
|
|
29
|
-
'a rebrand and every overlay read past it — it renders and nothing downstream knows it is there'
|
|
30
|
-
)
|
|
31
|
-
},
|
|
32
|
-
proves: {
|
|
33
|
-
wrong: [
|
|
34
|
-
'<div className="row" />',
|
|
35
|
-
'<button onClick={save}>save</button>',
|
|
36
|
-
'<svg:path d="M0 0" />',
|
|
37
|
-
],
|
|
38
|
-
right: [
|
|
39
|
-
'<Box.card />',
|
|
40
|
-
'<Column>{rows}</Column>',
|
|
41
|
-
'<Button.buttonPrimary label="save" />',
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
}
|