rvlib-shipkit 0.38.1 → 1.0.0
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 +367 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/ts/config.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// skip entries are repo-relative dir prefixes; the defaults below always apply.
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
14
14
|
import { join } from 'path'
|
|
15
|
+
import { gitRoot } from '../core/git-root.ts'
|
|
15
16
|
|
|
16
17
|
export type RvtPackage = {
|
|
17
18
|
name: string // package.json "name" — the import root for files inside
|
|
@@ -35,78 +36,12 @@ export const ARTIFACT_SKIP = ['dist', 'build', 'cjs', 'out']
|
|
|
35
36
|
export const DEFAULT_SKIP = [...ALWAYS_SKIP, ...ARTIFACT_SKIP]
|
|
36
37
|
|
|
37
38
|
export function findRepoRoot(cwd: string = process.cwd()): string {
|
|
38
|
-
|
|
39
|
-
const out = p.exitCode === 0 ? p.stdout.toString().trim() : ''
|
|
40
|
-
return out !== '' ? out : cwd
|
|
39
|
+
return gitRoot(cwd)?.top ?? cwd
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
// A quote-aware scanner rather than regexes, because tsconfig is the one
|
|
46
|
-
// dialect where the regex version is guaranteed to be wrong: `"src/**/*"` is
|
|
47
|
-
// a `/*` with its own `*/`, so a block-comment regex eats an arbitrary span of
|
|
48
|
-
// a perfectly valid file. Measured across ~/dev, the regex version THREW on 8
|
|
49
|
-
// real tsconfigs (this repo's included) and silently rewrote `"src/**/*.ts"`
|
|
50
|
-
// to `"src*.ts"` in 8 more. This parser gates typecheck AND publish, so it
|
|
51
|
-
// does not get to be loose.
|
|
52
|
-
export function parseJsonc(text: string): unknown {
|
|
53
|
-
let out = ''
|
|
54
|
-
let i = 0
|
|
55
|
-
while (i < text.length) {
|
|
56
|
-
const c = text[i]!
|
|
57
|
-
if (c === '"') {
|
|
58
|
-
out += c
|
|
59
|
-
i++
|
|
60
|
-
while (i < text.length) {
|
|
61
|
-
const s = text[i]!
|
|
62
|
-
out += s
|
|
63
|
-
i++
|
|
64
|
-
if (s === '\\') { out += text[i] ?? ''; i++; continue }
|
|
65
|
-
if (s === '"') break
|
|
66
|
-
}
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
if (c === '/' && text[i + 1] === '/') { while (i < text.length && text[i] !== '\n') i++; continue }
|
|
70
|
-
if (c === '/' && text[i + 1] === '*') {
|
|
71
|
-
i += 2
|
|
72
|
-
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++
|
|
73
|
-
i += 2
|
|
74
|
-
continue
|
|
75
|
-
}
|
|
76
|
-
out += c
|
|
77
|
-
i++
|
|
78
|
-
}
|
|
79
|
-
return JSON.parse(dropTrailingCommas(out))
|
|
80
|
-
}
|
|
42
|
+
import { parseJsonc } from './jsonc.ts'
|
|
81
43
|
|
|
82
|
-
|
|
83
|
-
function dropTrailingCommas(text: string): string {
|
|
84
|
-
let out = ''
|
|
85
|
-
let i = 0
|
|
86
|
-
let pendingComma = -1 // index in `out` of a comma awaiting its next token
|
|
87
|
-
while (i < text.length) {
|
|
88
|
-
const c = text[i]!
|
|
89
|
-
if (c === '"') {
|
|
90
|
-
if (pendingComma >= 0) pendingComma = -1
|
|
91
|
-
out += c
|
|
92
|
-
i++
|
|
93
|
-
while (i < text.length) {
|
|
94
|
-
const s = text[i]!
|
|
95
|
-
out += s
|
|
96
|
-
i++
|
|
97
|
-
if (s === '\\') { out += text[i] ?? ''; i++; continue }
|
|
98
|
-
if (s === '"') break
|
|
99
|
-
}
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
if (c === ',') { pendingComma = out.length; out += c; i++; continue }
|
|
103
|
-
if ((c === '}' || c === ']') && pendingComma >= 0) out = out.slice(0, pendingComma) + out.slice(pendingComma + 1)
|
|
104
|
-
if (!/\s/.test(c)) pendingComma = -1
|
|
105
|
-
out += c
|
|
106
|
-
i++
|
|
107
|
-
}
|
|
108
|
-
return out
|
|
109
|
-
}
|
|
44
|
+
export { parseJsonc }
|
|
110
45
|
|
|
111
46
|
function rootsFromTsconfig(root: string): string[] {
|
|
112
47
|
for (const name of ['tsconfig.base.json', 'tsconfig.json']) {
|
package/src/ts/jsonc.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// tsconfig files are JSONC: strip comments and trailing commas. Its own module
|
|
2
|
+
// with no disk access, so a caller that parses a tsconfig it was handed never
|
|
3
|
+
// imports the loader in ts/config.ts (core/raw-access.ts).
|
|
4
|
+
//
|
|
5
|
+
// A quote-aware scanner rather than regexes, because tsconfig is the one
|
|
6
|
+
// dialect where the regex version is guaranteed to be wrong: `"src/**/*"` is
|
|
7
|
+
// a `/*` with its own `*/`, so a block-comment regex eats an arbitrary span of
|
|
8
|
+
// a perfectly valid file. Measured across ~/dev, the regex version THREW on 8
|
|
9
|
+
// real tsconfigs (this repo's included) and silently rewrote `"src/**/*.ts"`
|
|
10
|
+
// to `"src*.ts"` in 8 more. This parser gates typecheck AND publish, so it
|
|
11
|
+
// does not get to be loose.
|
|
12
|
+
export function parseJsonc(text: string): unknown {
|
|
13
|
+
let out = ''
|
|
14
|
+
let i = 0
|
|
15
|
+
while (i < text.length) {
|
|
16
|
+
const c = text[i]!
|
|
17
|
+
if (c === '"') {
|
|
18
|
+
out += c
|
|
19
|
+
i++
|
|
20
|
+
while (i < text.length) {
|
|
21
|
+
const s = text[i]!
|
|
22
|
+
out += s
|
|
23
|
+
i++
|
|
24
|
+
if (s === '\\') { out += text[i] ?? ''; i++; continue }
|
|
25
|
+
if (s === '"') break
|
|
26
|
+
}
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if (c === '/' && text[i + 1] === '/') { while (i < text.length && text[i] !== '\n') i++; continue }
|
|
30
|
+
if (c === '/' && text[i + 1] === '*') {
|
|
31
|
+
i += 2
|
|
32
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++
|
|
33
|
+
i += 2
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
out += c
|
|
37
|
+
i++
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(dropTrailingCommas(out))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** a comma before the next `}` or `]`, outside any string, is dropped */
|
|
43
|
+
function dropTrailingCommas(text: string): string {
|
|
44
|
+
let out = ''
|
|
45
|
+
let i = 0
|
|
46
|
+
let pendingComma = -1 // index in `out` of a comma awaiting its next token
|
|
47
|
+
while (i < text.length) {
|
|
48
|
+
const c = text[i]!
|
|
49
|
+
if (c === '"') {
|
|
50
|
+
if (pendingComma >= 0) pendingComma = -1
|
|
51
|
+
out += c
|
|
52
|
+
i++
|
|
53
|
+
while (i < text.length) {
|
|
54
|
+
const s = text[i]!
|
|
55
|
+
out += s
|
|
56
|
+
i++
|
|
57
|
+
if (s === '\\') { out += text[i] ?? ''; i++; continue }
|
|
58
|
+
if (s === '"') break
|
|
59
|
+
}
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (c === ',') { pendingComma = out.length; out += c; i++; continue }
|
|
63
|
+
if ((c === '}' || c === ']') && pendingComma >= 0) out = out.slice(0, pendingComma) + out.slice(pendingComma + 1)
|
|
64
|
+
if (!/\s/.test(c)) pendingComma = -1
|
|
65
|
+
out += c
|
|
66
|
+
i++
|
|
67
|
+
}
|
|
68
|
+
return out
|
|
69
|
+
}
|
package/src/ts/lex.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// a small TypeScript lexer for guards that read source as TEXT: comments
|
|
2
|
+
// dropped, every string literal and template chunk pulled out as data, and the
|
|
3
|
+
// code left with a `\0<index>\0` placeholder where each one stood. It is not a
|
|
4
|
+
// parser. It knows the four things that make a naive regex lie: comments,
|
|
5
|
+
// template `${…}` nesting, regex literals, and an apostrophe inside JSX text.
|
|
6
|
+
//
|
|
7
|
+
// The constraint a reader breaks first: a `/` is a regex only where an
|
|
8
|
+
// expression can start, which is read off the last significant code character.
|
|
9
|
+
|
|
10
|
+
export type Lexed = {
|
|
11
|
+
/** the source without comments, each literal replaced by `\0<i>\0` */
|
|
12
|
+
code: string
|
|
13
|
+
/** the literal texts, escapes resolved, a template's chunks one entry each */
|
|
14
|
+
strings: string[]
|
|
15
|
+
/** per entry of `strings`: true for a template chunk, false for a quoted literal */
|
|
16
|
+
template: boolean[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SIMPLE_ESCAPES: Record<string, string> = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', '0': '\0' }
|
|
20
|
+
|
|
21
|
+
/** the escape at src[j] (the backslash): the text it stands for, and its length */
|
|
22
|
+
function unescapeAt(src: string, j: number): [string, number] {
|
|
23
|
+
const c = src[j + 1] ?? ''
|
|
24
|
+
if (c in SIMPLE_ESCAPES) return [SIMPLE_ESCAPES[c]!, 2]
|
|
25
|
+
if (c === 'x' && /^[\da-f]{2}$/i.test(src.slice(j + 2, j + 4))) return [String.fromCharCode(parseInt(src.slice(j + 2, j + 4), 16)), 4]
|
|
26
|
+
if (c === 'u' && src[j + 2] === '{') {
|
|
27
|
+
const end = src.indexOf('}', j + 3)
|
|
28
|
+
const cp = parseInt(src.slice(j + 3, end), 16)
|
|
29
|
+
if (end > 0 && Number.isFinite(cp)) return [String.fromCodePoint(cp), end - j + 1]
|
|
30
|
+
}
|
|
31
|
+
if (c === 'u' && /^[\da-f]{4}$/i.test(src.slice(j + 2, j + 6))) return [String.fromCharCode(parseInt(src.slice(j + 2, j + 6), 16)), 6]
|
|
32
|
+
if (c === '\n') return ['', 2]
|
|
33
|
+
return [c, 2]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const REGEX_BEFORE = new Set(['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '<', '>', '~', '^'])
|
|
37
|
+
const REGEX_AFTER_WORD = /(?:^|[^\w$.])(?:return|typeof|case|do|else|in|of|void|yield|await|delete|throw|new)$/
|
|
38
|
+
|
|
39
|
+
export function lexSource(src: string, o: { jsx?: boolean } = {}): Lexed {
|
|
40
|
+
const strings: string[] = []
|
|
41
|
+
const template: boolean[] = []
|
|
42
|
+
let code = ''
|
|
43
|
+
let i = 0
|
|
44
|
+
const put = (text: string, isTemplate: boolean): void => { strings.push(text); template.push(isTemplate); code += `\0${strings.length - 1}\0` }
|
|
45
|
+
const lastSignificant = (): string => { const t = code.trimEnd(); return t.at(-1) ?? '' }
|
|
46
|
+
|
|
47
|
+
/** code until the `}` closing a `${`, at brace depth 0 */
|
|
48
|
+
const codeUntilClose = (): void => {
|
|
49
|
+
let depth = 0
|
|
50
|
+
while (i < src.length) {
|
|
51
|
+
const ch = src[i]!
|
|
52
|
+
if (ch === '}' && depth === 0) { i++; return }
|
|
53
|
+
if (ch === '{') depth++
|
|
54
|
+
if (ch === '}') depth--
|
|
55
|
+
if (!step()) { code += ch; i++ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const readTemplate = (): void => {
|
|
60
|
+
i++ // the opening backtick
|
|
61
|
+
let text = ''
|
|
62
|
+
while (i < src.length && src[i] !== '`') {
|
|
63
|
+
if (src[i] === '\\') { const [t, n] = unescapeAt(src, i); text += t; i += n; continue }
|
|
64
|
+
if (src[i] === '$' && src[i + 1] === '{') {
|
|
65
|
+
put(text, true)
|
|
66
|
+
text = ''
|
|
67
|
+
i += 2
|
|
68
|
+
code += '${'
|
|
69
|
+
codeUntilClose()
|
|
70
|
+
code += '}'
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
text += src[i]
|
|
74
|
+
i++
|
|
75
|
+
}
|
|
76
|
+
i++ // the closing backtick
|
|
77
|
+
put(text, true)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** one lexical element that is not plain code; false when src[i] is plain */
|
|
81
|
+
const step = (): boolean => {
|
|
82
|
+
const ch = src[i]!
|
|
83
|
+
const next = src[i + 1]
|
|
84
|
+
if (ch === '/' && next === '/') {
|
|
85
|
+
while (i < src.length && src[i] !== '\n') i++
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
if (ch === '/' && next === '*') {
|
|
89
|
+
const end = src.indexOf('*/', i + 2)
|
|
90
|
+
i = end < 0 ? src.length : end + 2
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
if (ch === '`') { code += '`'; readTemplate(); return true }
|
|
94
|
+
if (ch === "'" || ch === '"') {
|
|
95
|
+
// an apostrophe right after a letter is JSX text (`don't`), never a
|
|
96
|
+
// string: in code a literal never follows an identifier character
|
|
97
|
+
if (o.jsx === true && /[\p{L}\d]/u.test(src[i - 1] ?? '')) return false
|
|
98
|
+
let j = i + 1
|
|
99
|
+
let text = ''
|
|
100
|
+
while (j < src.length && src[j] !== ch && src[j] !== '\n') {
|
|
101
|
+
if (src[j] === '\\') { const [t, n] = unescapeAt(src, j); text += t; j += n; continue }
|
|
102
|
+
text += src[j]
|
|
103
|
+
j++
|
|
104
|
+
}
|
|
105
|
+
put(text, false)
|
|
106
|
+
i = j + 1
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
if (ch === '/') {
|
|
110
|
+
const last = lastSignificant()
|
|
111
|
+
// `</tag>` and `<Tag />` are markup, never a regex
|
|
112
|
+
if (o.jsx === true && (code.endsWith('<') || next === '>')) return false
|
|
113
|
+
if (!REGEX_BEFORE.has(last) && !REGEX_AFTER_WORD.test(code.trimEnd())) return false
|
|
114
|
+
let j = i + 1
|
|
115
|
+
let inClass = false
|
|
116
|
+
while (j < src.length && src[j] !== '\n') {
|
|
117
|
+
if (src[j] === '\\') { j += 2; continue }
|
|
118
|
+
if (src[j] === '[') inClass = true
|
|
119
|
+
else if (src[j] === ']') inClass = false
|
|
120
|
+
else if (src[j] === '/' && !inClass) break
|
|
121
|
+
j++
|
|
122
|
+
}
|
|
123
|
+
j++
|
|
124
|
+
while (j < src.length && /[a-z]/.test(src[j]!)) j++
|
|
125
|
+
code += '/re/'
|
|
126
|
+
i = j
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
while (i < src.length) {
|
|
133
|
+
if (!step()) { code += src[i]; i++ }
|
|
134
|
+
}
|
|
135
|
+
return { code, strings, template }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** the JSX text runs of lexed code: what sits between a tag or an `{…}`
|
|
139
|
+
* expression and the next one. A run that reads as code (an assignment, a
|
|
140
|
+
* statement end, a logical operator) is code between two tags, and is left out */
|
|
141
|
+
export function jsxTexts(code: string): string[] {
|
|
142
|
+
return [...code.matchAll(/[>}]([^<>{}\0]*)[<{]/g)].map((m) => m[1]!).filter((t) => /\p{L}/u.test(t) && !/[=;]|&&|\|\||\breturn\b/.test(t))
|
|
143
|
+
}
|
package/src/ts/rename.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Rename / move a file or folder AND update every
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// Rename / move a file or folder AND update every reference to it across all
|
|
2
|
+
// tracked + untracked source files: every exact occurrence of its repo-relative
|
|
3
|
+
// path (or package-name path), and every RELATIVE import in a code file that
|
|
4
|
+
// resolves to what moves. A relative specifier only means something from the
|
|
5
|
+
// file that writes it, so each one is resolved from its importer and written
|
|
6
|
+
// again from where both ends land: an importer of the moved file, and the moved
|
|
7
|
+
// file's own imports of what stays behind.
|
|
7
8
|
//
|
|
8
9
|
// Modes:
|
|
9
10
|
// file -> file path renames the file
|
|
@@ -13,6 +14,10 @@
|
|
|
13
14
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs'
|
|
14
15
|
import { basename, dirname, extname, join, relative, resolve } from 'path'
|
|
15
16
|
import { canonicalSpecifier, isSkipped, loadConfig, toPosix, type RvtConfig } from 'rvlib-shipkit/src/ts/config.ts'
|
|
17
|
+
import { extractSpecs } from 'rvlib-shipkit/src/ts/source-graph.ts'
|
|
18
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
19
|
+
|
|
20
|
+
const CODE_RE = /\.[cm]?[jt]sx?$/
|
|
16
21
|
|
|
17
22
|
const BINARY_EXTS = new Set([
|
|
18
23
|
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.tiff',
|
|
@@ -93,9 +98,37 @@ export function runRename(srcArg: string, dstArg: string, opts: { apply?: boolea
|
|
|
93
98
|
|
|
94
99
|
const pkgDirs = cfg.packages.map((p) => p.dir)
|
|
95
100
|
// scan all tracked + untracked (non-ignored) files
|
|
96
|
-
const ls = Bun.spawnSync([
|
|
101
|
+
const ls = Bun.spawnSync([gitBin(), 'ls-files', '-co', '--exclude-standard', '-z'], { cwd: repoRoot })
|
|
97
102
|
const allFiles = ls.stdout.toString().split('\u0000').filter(Boolean)
|
|
98
103
|
|
|
104
|
+
const movedTo = (rel: string): string | null =>
|
|
105
|
+
rel === srcRel ? dstRel : srcIsFolder && rel.startsWith(`${srcRel}/`) ? `${dstRel}${rel.slice(srcRel.length)}` : null
|
|
106
|
+
const isFile = (rel: string): boolean => {
|
|
107
|
+
try { return statSync(join(repoRoot, rel)).isFile() } catch { return false }
|
|
108
|
+
}
|
|
109
|
+
const rewriteFile = (file: string, content: string): { text: string; count: number } => {
|
|
110
|
+
const rel = CODE_RE.test(file) ? relativeRewrites(file, content, movedTo, isFile) : new Map<string, string>()
|
|
111
|
+
if (rel.size === 0) return scanPairs(content, pairs)
|
|
112
|
+
// the relative specifiers this move retargets are set aside while the
|
|
113
|
+
// path scan runs, so the two rewrites can never compound on one string
|
|
114
|
+
let masked = content
|
|
115
|
+
let relCount = 0
|
|
116
|
+
const holes: string[] = []
|
|
117
|
+
for (const [from, to] of rel) {
|
|
118
|
+
for (const q of ["'", '"']) {
|
|
119
|
+
const parts = masked.split(`${q}${from}${q}`)
|
|
120
|
+
if (parts.length === 1) continue
|
|
121
|
+
relCount += parts.length - 1
|
|
122
|
+
masked = parts.join(`\u0000mv${holes.length}\u0000`)
|
|
123
|
+
holes.push(`${q}${to}${q}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const scanned = scanPairs(masked, pairs)
|
|
127
|
+
let text = scanned.text
|
|
128
|
+
holes.forEach((h, i) => { text = text.split(`\u0000mv${i}\u0000`).join(h) })
|
|
129
|
+
return { text, count: scanned.count + relCount }
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
const hits: { file: string; count: number }[] = []
|
|
100
133
|
let totalRefs = 0
|
|
101
134
|
for (const file of allFiles) {
|
|
@@ -109,7 +142,7 @@ export function runRename(srcArg: string, dstArg: string, opts: { apply?: boolea
|
|
|
109
142
|
}
|
|
110
143
|
const nul = content.indexOf('\u0000')
|
|
111
144
|
if (nul !== -1 && nul < 4096) continue // cheap binary sniff
|
|
112
|
-
const count =
|
|
145
|
+
const count = rewriteFile(file, content).count
|
|
113
146
|
if (count > 0) {
|
|
114
147
|
hits.push({ file, count })
|
|
115
148
|
totalRefs += count
|
|
@@ -140,12 +173,12 @@ export function runRename(srcArg: string, dstArg: string, opts: { apply?: boolea
|
|
|
140
173
|
for (const h of hits) {
|
|
141
174
|
const full = join(repoRoot, h.file)
|
|
142
175
|
const before = readFileSync(full, 'utf8')
|
|
143
|
-
const after =
|
|
176
|
+
const after = rewriteFile(h.file, before).text
|
|
144
177
|
if (after !== before) writeFileSync(full, after)
|
|
145
178
|
}
|
|
146
179
|
const dstDir = dirname(dstAbs)
|
|
147
180
|
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true })
|
|
148
|
-
const mv = Bun.spawnSync([
|
|
181
|
+
const mv = Bun.spawnSync([gitBin(), 'mv', srcAbs, dstAbs], { cwd: repoRoot, stdout: 'pipe', stderr: 'pipe' })
|
|
149
182
|
if (mv.exitCode !== 0) renameSync(srcAbs, dstAbs) // untracked file: plain fs move
|
|
150
183
|
}
|
|
151
184
|
|
|
@@ -179,6 +212,36 @@ export function renameCli(argv: string[]): number {
|
|
|
179
212
|
return 0
|
|
180
213
|
}
|
|
181
214
|
|
|
215
|
+
/** the relative specifiers of one code file that this move retargets, each
|
|
216
|
+
* mapped to the specifier that names the same file after the move, written in
|
|
217
|
+
* the same style (explicit extension, `.js` for a `.ts` file, no extension, or
|
|
218
|
+
* a folder's index) */
|
|
219
|
+
function relativeRewrites(file: string, content: string, movedTo: (rel: string) => string | null, isFile: (rel: string) => boolean): Map<string, string> {
|
|
220
|
+
const out = new Map<string, string>()
|
|
221
|
+
const fromAfter = movedTo(file) ?? file
|
|
222
|
+
for (const spec of new Set(extractSpecs(content))) {
|
|
223
|
+
if (!spec.startsWith('./') && !spec.startsWith('../')) continue
|
|
224
|
+
const base = toPosix(join(dirname(file), spec))
|
|
225
|
+
const forms: [string, (p: string) => string][] = [
|
|
226
|
+
[base, (p) => p],
|
|
227
|
+
[base.replace(/\.js$/, '.ts'), (p) => p.replace(/\.ts$/, '.js')],
|
|
228
|
+
[`${base}.ts`, (p) => p.replace(/\.ts$/, '')],
|
|
229
|
+
[`${base}.tsx`, (p) => p.replace(/\.tsx$/, '')],
|
|
230
|
+
[`${base}/index.ts`, (p) => p.replace(/\/index\.ts$/, '')],
|
|
231
|
+
[`${base}/index.tsx`, (p) => p.replace(/\/index\.tsx$/, '')],
|
|
232
|
+
]
|
|
233
|
+
const hit = forms.find(([candidate]) => isFile(candidate))
|
|
234
|
+
if (hit == null) continue
|
|
235
|
+
const [target, restyle] = hit
|
|
236
|
+
const targetAfter = movedTo(target) ?? target
|
|
237
|
+
if (targetAfter === target && fromAfter === file) continue
|
|
238
|
+
let next = toPosix(relative(dirname(fromAfter), restyle(targetAfter)))
|
|
239
|
+
if (!next.startsWith('../')) next = `./${next}`
|
|
240
|
+
if (next !== spec) out.set(spec, next)
|
|
241
|
+
}
|
|
242
|
+
return out
|
|
243
|
+
}
|
|
244
|
+
|
|
182
245
|
// simultaneous multi-needle replace: earliest match wins (longest needle on a
|
|
183
246
|
// tie) and the scan continues PAST the replacement — sequential split/join
|
|
184
247
|
// would let one pair's output feed the next (the package form is a substring
|
package/src/ts/source-graph.ts
CHANGED
|
@@ -32,7 +32,23 @@ export function crawlSources(cfg: RvtConfig, dir: string, out: string[]): void {
|
|
|
32
32
|
// strip comments (line comments only when preceded by whitespace, so
|
|
33
33
|
// 'http://...' survives) and type-only import/export statements, then extract
|
|
34
34
|
// the remaining runtime specifiers
|
|
35
|
-
export function extractSpecs(content: string): string[] {
|
|
35
|
+
export function extractSpecs(content: string, o: { dynamic?: boolean } = {}): string[] {
|
|
36
|
+
const memo = o.dynamic === false ? staticSpecs : allSpecs
|
|
37
|
+
const hit = memo.get(content)
|
|
38
|
+
if (hit != null) return [...hit]
|
|
39
|
+
const specs = scanSpecs(content, o)
|
|
40
|
+
if (memo.size >= SPECS_MEMO_MAX) memo.clear()
|
|
41
|
+
memo.set(content, specs)
|
|
42
|
+
return [...specs]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** extractSpecs is a pure function of the text, and every gate asks it of the
|
|
46
|
+
* same check closures again, so the answer is remembered per text */
|
|
47
|
+
const allSpecs = new Map<string, string[]>()
|
|
48
|
+
const staticSpecs = new Map<string, string[]>()
|
|
49
|
+
const SPECS_MEMO_MAX = 4000
|
|
50
|
+
|
|
51
|
+
function scanSpecs(content: string, o: { dynamic?: boolean }): string[] {
|
|
36
52
|
// template-literal content is text, a codegen emitting import lines inside
|
|
37
53
|
// a backtick block is not an import of this module. single-line templates
|
|
38
54
|
// strip in place, multiline ones blank via the shared mask (null mask =
|
|
@@ -40,9 +56,11 @@ export function extractSpecs(content: string): string[] {
|
|
|
40
56
|
const rawLines = content.split('\n')
|
|
41
57
|
const mask = templateLineMask(rawLines)
|
|
42
58
|
const demasked = mask == null ? content : rawLines.map((l, i) => (mask[i] ? '' : l)).join('\n')
|
|
59
|
+
// ONE left to right pass over both comment shapes: whichever opens first
|
|
60
|
+
// owns the text, so a `/*` inside a line comment ("src/*") never starts a
|
|
61
|
+
// block comment that swallows the imports below it
|
|
43
62
|
const cleaned = demasked
|
|
44
|
-
.replace(/\/\*[\s\S]
|
|
45
|
-
.replace(/(^|\s)\/\/[^\n]*/g, '$1')
|
|
63
|
+
.replace(/\/\*[\s\S]*?\*\/|(^|\s)\/\/[^\n]*/g, (m, lead: string | undefined) => (m.startsWith('/*') ? '' : lead ?? ''))
|
|
46
64
|
.replace(/`(?:\\.|[^`\\\n])*`/g, '``')
|
|
47
65
|
.replace(/\b(?:import|export)\s+type\s[^'"]*?from\s*(['"])[^'"\n]*\1/g, '')
|
|
48
66
|
const specs: string[] = []
|
|
@@ -57,7 +75,9 @@ export function extractSpecs(content: string): string[] {
|
|
|
57
75
|
}
|
|
58
76
|
for (const re of [
|
|
59
77
|
/\bimport\s*['"]([^'"\n]+)['"]/g, // side-effect import
|
|
60
|
-
|
|
78
|
+
// dynamic import / require. `dynamic: false` leaves them out: what a
|
|
79
|
+
// module's LOAD runs, not what a function it exports may run later
|
|
80
|
+
...(o.dynamic === false ? [] : [/\b(?:import|require)\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g]),
|
|
61
81
|
]) {
|
|
62
82
|
for (const m of cleaned.matchAll(re)) {
|
|
63
83
|
if (m[1] != null) specs.push(m[1])
|
|
@@ -120,13 +140,13 @@ export function buildSourceGraph(cfg: RvtConfig = loadConfig()): SourceGraph {
|
|
|
120
140
|
* the transitive import closure of `entries`, entries included, WITHOUT
|
|
121
141
|
* crawling the repo: files are read only as the walk reaches them.
|
|
122
142
|
*
|
|
123
|
-
* This is what puts a
|
|
143
|
+
* This is what puts a check's own code in its cache key. A check that imports a
|
|
124
144
|
* shared helper carries that helper's identity, so fixing the helper moves the
|
|
125
145
|
* check's key. An entry that does not exist is dropped: the caller already
|
|
126
|
-
* knows a missing declared path makes a
|
|
146
|
+
* knows a missing declared path makes a check uncacheable, and a throw here
|
|
127
147
|
* would turn a stale declaration into a dead gate.
|
|
128
148
|
*/
|
|
129
|
-
export function importClosure(cfg: RvtConfig, entries: string[]): string[] {
|
|
149
|
+
export function importClosure(cfg: RvtConfig, entries: string[], o: { dynamic?: boolean } = {}): string[] {
|
|
130
150
|
const probe = new Map<string, boolean>()
|
|
131
151
|
const has = (p: string): boolean => {
|
|
132
152
|
let r = probe.get(p)
|
|
@@ -147,7 +167,7 @@ export function importClosure(cfg: RvtConfig, entries: string[]): string[] {
|
|
|
147
167
|
} catch {
|
|
148
168
|
continue
|
|
149
169
|
}
|
|
150
|
-
for (const spec of extractSpecs(content)) {
|
|
170
|
+
for (const spec of extractSpecs(content, o)) {
|
|
151
171
|
const target = resolveSpec(cfg, file, spec, has)
|
|
152
172
|
if (target == null || seen.has(target)) continue
|
|
153
173
|
seen.add(target)
|
|
@@ -1,101 +1,10 @@
|
|
|
1
|
-
// which tsconfig files a typecheck must actually visit.
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// `ok: typecheck` while a type error sits in src, which is the single worst
|
|
6
|
-
// output this repo can produce: an unmeasured green nobody can tell from a
|
|
7
|
-
// measured one. tsc will not warn about it, so the caller has to resolve the
|
|
8
|
-
// leaves itself and refuse when there are none.
|
|
9
|
-
import { existsSync, readFileSync, statSync } from 'fs'
|
|
10
|
-
import { dirname, isAbsolute, join, resolve } from 'path'
|
|
11
|
-
import { parseJsonc } from 'rvlib-shipkit/src/ts/config.ts'
|
|
1
|
+
// which tsconfig files a typecheck must actually visit, read from the disk.
|
|
2
|
+
// ts/tsconfig-resolve.ts owns the resolution and why it exists.
|
|
3
|
+
import { fsUnitReader } from '../release/units.ts'
|
|
4
|
+
import { resolveTypecheckProjects as resolveWith, type TypecheckProjects } from './tsconfig-resolve.ts'
|
|
12
5
|
|
|
13
|
-
export type TypecheckProjects
|
|
14
|
-
/** absolute paths, in declaration order, of the configs that compile files */
|
|
15
|
-
projects: string[]
|
|
16
|
-
/** nothing would be compiled: the check would pass without checking */
|
|
17
|
-
vacuous: boolean
|
|
18
|
-
/** references pointing at a path that does not exist, verbatim as declared */
|
|
19
|
-
missing: string[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type RawConfig = {
|
|
23
|
-
files?: unknown
|
|
24
|
-
include?: unknown
|
|
25
|
-
references?: { path?: unknown }[]
|
|
26
|
-
}
|
|
6
|
+
export type { TypecheckProjects } from './tsconfig-resolve.ts'
|
|
27
7
|
|
|
28
|
-
/** a reference may name a directory, a config file, or a dir with no config */
|
|
29
|
-
function configPathFor(base: string, declared: string): string | null {
|
|
30
|
-
const p = isAbsolute(declared) ? declared : resolve(base, declared)
|
|
31
|
-
if (!existsSync(p)) return null
|
|
32
|
-
try { if (statSync(p).isDirectory()) return existsSync(join(p, 'tsconfig.json')) ? join(p, 'tsconfig.json') : null }
|
|
33
|
-
catch { return null }
|
|
34
|
-
return p
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* whether tsc would compile anything for THIS config, ignoring its references.
|
|
39
|
-
*
|
|
40
|
-
* tsc's rule, and every branch here is a shape found in the field:
|
|
41
|
-
* - `files` non-empty, or `include` non-empty: compiles those
|
|
42
|
-
* - neither key present: the default `include` is `**\/*`, so it compiles the
|
|
43
|
-
* whole directory tree, which is why a bare `{ references: [...] }` is NOT
|
|
44
|
-
* a solution config
|
|
45
|
-
* - `files: []` with no `include`: the solution-style marker, compiles nothing
|
|
46
|
-
* - `include: []`: compiles nothing whatever `files` says, unless `files` lists
|
|
47
|
-
* something
|
|
48
|
-
*/
|
|
49
|
-
function compilesOwnFiles(cfg: RawConfig): boolean {
|
|
50
|
-
const files = Array.isArray(cfg.files) ? cfg.files : null
|
|
51
|
-
const include = Array.isArray(cfg.include) ? cfg.include : null
|
|
52
|
-
if (files != null && files.length > 0) return true
|
|
53
|
-
if (include != null) return include.length > 0
|
|
54
|
-
return files == null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function read(path: string): RawConfig {
|
|
58
|
-
let text: string
|
|
59
|
-
try { text = readFileSync(path, 'utf8') }
|
|
60
|
-
catch (e) { throw new Error(`tsconfig unreadable at ${path}: ${String(e).slice(0, 120)}`) }
|
|
61
|
-
try { return (parseJsonc(text) ?? {}) as RawConfig }
|
|
62
|
-
catch (e) { throw new Error(`tsconfig unparseable at ${path}: ${String(e).slice(0, 120)}`) }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Walk `references` to the leaves. A leaf that declares `files: []` with no
|
|
67
|
-
* `include` compiles nothing and is dropped, so a solution whose every leaf is
|
|
68
|
-
* empty comes back `vacuous` rather than as a command that passes for free.
|
|
69
|
-
*/
|
|
70
8
|
export function resolveTypecheckProjects(tsconfigPath: string): TypecheckProjects {
|
|
71
|
-
|
|
72
|
-
const projects: string[] = []
|
|
73
|
-
const missing: string[] = []
|
|
74
|
-
// a reference loop is legal to WRITE and would otherwise recurse forever
|
|
75
|
-
const seen = new Set<string>()
|
|
76
|
-
|
|
77
|
-
const visit = (path: string): void => {
|
|
78
|
-
if (seen.has(path)) return
|
|
79
|
-
seen.add(path)
|
|
80
|
-
const cfg = read(path)
|
|
81
|
-
// references are checked FIRST so the list reads dependencies before
|
|
82
|
-
// dependents, the order `tsc -b` would build in
|
|
83
|
-
for (const r of Array.isArray(cfg.references) ? cfg.references : []) {
|
|
84
|
-
const declared = typeof r?.path === 'string' ? r.path : null
|
|
85
|
-
if (declared == null) continue
|
|
86
|
-
const next = configPathFor(dirname(path), declared)
|
|
87
|
-
if (next == null) missing.push(declared)
|
|
88
|
-
else visit(next)
|
|
89
|
-
}
|
|
90
|
-
// `references` does NOT suppress a config's own inputs: only an empty
|
|
91
|
-
// `files` with no `include` does, and that is what makes a config
|
|
92
|
-
// solution-style. Skipping a referencing config outright is how the
|
|
93
|
-
// first version of this file reintroduced the false green it exists to
|
|
94
|
-
// kill, on `{ include: ['src'], references: [...] }`, which is the
|
|
95
|
-
// ordinary monorepo package shape
|
|
96
|
-
if (compilesOwnFiles(cfg)) projects.push(path)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
visit(start)
|
|
100
|
-
return { projects, vacuous: projects.length === 0, missing }
|
|
9
|
+
return resolveWith(tsconfigPath, fsUnitReader('/'), '/')
|
|
101
10
|
}
|