@invariant-ui/cli 0.0.0-stage → 0.2.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/LICENSE +105 -0
- package/README.md +250 -2
- package/dist/agent-kit/apply.js +232 -0
- package/dist/agent-kit/guidance.js +236 -0
- package/dist/agent-kit/index.js +5 -0
- package/dist/agent-kit/integrations.js +121 -0
- package/dist/agent-kit/plugin.js +115 -0
- package/dist/artifact-generator/index.js +75 -0
- package/dist/browser-verify/driver.js +2 -0
- package/dist/browser-verify/index.js +4 -0
- package/dist/browser-verify/playwright.js +82 -0
- package/dist/browser-verify/runner.js +175 -0
- package/dist/browser-verify/spec.js +103 -0
- package/dist/cli/args.js +77 -0
- package/dist/cli/baseline.js +64 -0
- package/dist/cli/cli.js +121 -0
- package/dist/cli/commands-browser.js +104 -0
- package/dist/cli/commands-discover.js +312 -0
- package/dist/cli/commands-enforcement.js +35 -0
- package/dist/cli/commands-governance.js +179 -0
- package/dist/cli/commands-hook.js +269 -0
- package/dist/cli/commands-patterns.js +324 -0
- package/dist/cli/commands-setup.js +485 -0
- package/dist/cli/commands-validate.js +157 -0
- package/dist/cli/commands.js +517 -0
- package/dist/cli/config.js +197 -0
- package/dist/cli/diff.js +66 -0
- package/dist/cli/enforcement-log.js +83 -0
- package/dist/cli/enforcement-report.js +96 -0
- package/dist/cli/errors.js +10 -0
- package/dist/cli/format.js +22 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/io.js +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/cli/pipeline.js +262 -0
- package/dist/cli/publish.js +77 -0
- package/dist/cli/report.js +127 -0
- package/dist/cli/starter-rules.js +57 -0
- package/dist/cli/store.js +60 -0
- package/dist/cli/version.js +5 -0
- package/dist/cli/watch.js +68 -0
- package/dist/cli/workflow.js +52 -0
- package/dist/compiler-angular/adapter.js +115 -0
- package/dist/compiler-angular/analyze.js +222 -0
- package/dist/compiler-angular/classes.js +446 -0
- package/dist/compiler-angular/declared.js +285 -0
- package/dist/compiler-angular/emit.js +183 -0
- package/dist/compiler-angular/index.js +4 -0
- package/dist/compiler-angular/template.js +142 -0
- package/dist/compiler-core/adapter.js +2 -0
- package/dist/compiler-core/adapters/inventory.js +312 -0
- package/dist/compiler-core/compile.js +148 -0
- package/dist/compiler-core/component-facts.js +123 -0
- package/dist/compiler-core/discovery/docs.js +66 -0
- package/dist/compiler-core/discovery/entries.js +270 -0
- package/dist/compiler-core/discovery/exports.js +207 -0
- package/dist/compiler-core/discovery/graph.js +59 -0
- package/dist/compiler-core/discovery/index.js +103 -0
- package/dist/compiler-core/discovery/packages.js +148 -0
- package/dist/compiler-core/discovery/schema.js +144 -0
- package/dist/compiler-core/discovery/storybook.js +232 -0
- package/dist/compiler-core/discovery/support.js +82 -0
- package/dist/compiler-core/discovery/tokens.js +186 -0
- package/dist/compiler-core/discovery/tooling.js +146 -0
- package/dist/compiler-core/fs.js +89 -0
- package/dist/compiler-core/glob.js +3 -0
- package/dist/compiler-core/index.js +16 -0
- package/dist/compiler-core/ir-builder.js +68 -0
- package/dist/compiler-core/json-schema.js +11 -0
- package/dist/compiler-core/testing/index.js +81 -0
- package/dist/compiler-core/ts/ast.js +166 -0
- package/dist/compiler-core/ts/members.js +163 -0
- package/dist/compiler-core/ts/program.js +81 -0
- package/dist/compiler-core/ts/static-eval.js +145 -0
- package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
- package/dist/compiler-core/walk.js +46 -0
- package/dist/compiler-ir/index.js +4 -0
- package/dist/compiler-ir/ir.js +67 -0
- package/dist/compiler-ir/json-schema.js +6 -0
- package/dist/compiler-ir/validate.js +116 -0
- package/dist/compiler-react/adapter.js +147 -0
- package/dist/compiler-react/analyze.js +199 -0
- package/dist/compiler-react/behavior.js +75 -0
- package/dist/compiler-react/detect.js +558 -0
- package/dist/compiler-react/emit.js +188 -0
- package/dist/compiler-react/index.js +2 -0
- package/dist/compiler-react/props.js +157 -0
- package/dist/compiler-react/styling.js +138 -0
- package/dist/compiler-react/usage.js +116 -0
- package/dist/compiler-react/variants.js +111 -0
- package/dist/compiler-storybook/adapter.js +253 -0
- package/dist/compiler-storybook/csf.js +521 -0
- package/dist/compiler-storybook/emit.js +202 -0
- package/dist/compiler-storybook/index-json.js +41 -0
- package/dist/compiler-storybook/index.js +5 -0
- package/dist/compiler-storybook/mdx.js +120 -0
- package/dist/compiler-tokens/adapter.js +158 -0
- package/dist/compiler-tokens/css.js +59 -0
- package/dist/compiler-tokens/index.js +5 -0
- package/dist/compiler-tokens/json-lines.js +106 -0
- package/dist/compiler-tokens/json-tokens.js +52 -0
- package/dist/compiler-vue/adapter.js +107 -0
- package/dist/compiler-vue/analyze.js +344 -0
- package/dist/compiler-vue/declared.js +343 -0
- package/dist/compiler-vue/emit.js +129 -0
- package/dist/compiler-vue/index.js +5 -0
- package/dist/compiler-vue/script.js +472 -0
- package/dist/compiler-vue/sfc.js +44 -0
- package/dist/compiler-vue/template.js +73 -0
- package/dist/contract/archetype-checks.js +153 -0
- package/dist/contract/components.js +102 -0
- package/dist/contract/conflicts.js +39 -0
- package/dist/contract/contract.js +46 -0
- package/dist/contract/evidence.js +84 -0
- package/dist/contract/fact-path.js +39 -0
- package/dist/contract/facts.js +62 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/integrity.js +21 -0
- package/dist/contract/json-schema.js +11 -0
- package/dist/contract/knowledge.js +95 -0
- package/dist/contract/pattern-checks.js +254 -0
- package/dist/contract/patterns.js +225 -0
- package/dist/contract/primitives.js +81 -0
- package/dist/contract/sources.js +30 -0
- package/dist/contract/tokens.js +41 -0
- package/dist/contract/validate.js +253 -0
- package/dist/contract-query/archetype.js +188 -0
- package/dist/contract-query/index.js +6 -0
- package/dist/contract-query/patterns.js +298 -0
- package/dist/contract-query/search.js +132 -0
- package/dist/contract-query/system.js +148 -0
- package/dist/contract-query/views.js +288 -0
- package/dist/governance/decisions.js +229 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/patterns-file.js +156 -0
- package/dist/governance/patterns.js +510 -0
- package/dist/governance/policies.js +148 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/loader.js +70 -0
- package/dist/mcp/protocol.js +27 -0
- package/dist/mcp/resources.js +138 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/stdio.js +28 -0
- package/dist/mcp/tools.js +267 -0
- package/dist/reconciliation/code.js +62 -0
- package/dist/reconciliation/components.js +401 -0
- package/dist/reconciliation/conflicts.js +126 -0
- package/dist/reconciliation/facts.js +41 -0
- package/dist/reconciliation/index.js +7 -0
- package/dist/reconciliation/reconcile.js +134 -0
- package/dist/reconciliation/resolve.js +158 -0
- package/dist/reconciliation/review.js +81 -0
- package/dist/reconciliation/storybook.js +370 -0
- package/dist/reconciliation/tokens.js +179 -0
- package/dist/reconciliation/util.js +55 -0
- package/dist/semantic-engine/additive.js +51 -0
- package/dist/semantic-engine/compounds.js +91 -0
- package/dist/semantic-engine/documents.js +157 -0
- package/dist/semantic-engine/enrich.js +42 -0
- package/dist/semantic-engine/index.js +7 -0
- package/dist/semantic-engine/infer.js +38 -0
- package/dist/semantic-engine/tokens.js +120 -0
- package/dist/shared/canonical.js +65 -0
- package/dist/shared/diagnostic.js +78 -0
- package/dist/shared/glob.js +98 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/json.js +2 -0
- package/dist/validation/card.js +50 -0
- package/dist/validation/discovery/arrangements.js +162 -0
- package/dist/validation/discovery/discover.js +70 -0
- package/dist/validation/discovery/index.js +8 -0
- package/dist/validation/discovery/model.js +127 -0
- package/dist/validation/discovery/promote.js +24 -0
- package/dist/validation/discovery/reviews.js +63 -0
- package/dist/validation/discovery/shapes.js +47 -0
- package/dist/validation/discovery/similar-files.js +146 -0
- package/dist/validation/discovery/util.js +47 -0
- package/dist/validation/index.js +11 -0
- package/dist/validation/interaction-facts.js +341 -0
- package/dist/validation/interaction-workflows.js +633 -0
- package/dist/validation/literals.js +251 -0
- package/dist/validation/pattern-rules.js +608 -0
- package/dist/validation/policy.js +241 -0
- package/dist/validation/report.js +163 -0
- package/dist/validation/rules.js +269 -0
- package/dist/validation/scan.js +295 -0
- package/dist/validation/text.js +34 -0
- package/dist/validation/validate-code.js +513 -0
- package/dist/validation/validate-usage.js +196 -0
- package/package.json +38 -4
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { renderArtifacts } from "../artifact-generator/index.js";
|
|
4
|
+
import { angularAdapter } from "../compiler-angular/index.js";
|
|
5
|
+
import { compile, INVENTORY_ADAPTERS, nodeFs, toArtifactEntries } from "../compiler-core/index.js";
|
|
6
|
+
import { reactAdapter } from "../compiler-react/index.js";
|
|
7
|
+
import { storybookAdapter } from "../compiler-storybook/index.js";
|
|
8
|
+
import { designTokensAdapter } from "../compiler-tokens/index.js";
|
|
9
|
+
import { vueAdapter } from "../compiler-vue/index.js";
|
|
10
|
+
import { applyDecisions, compilePatterns, compilePolicies, DECISIONS_FILE, PATTERNS_FILE, POLICIES_FILE, parseDecisions, parsePatterns, parsePolicies } from "../governance/index.js";
|
|
11
|
+
import { buildReviewQueue, finalizeAndVerify, reconcile } from "../reconciliation/index.js";
|
|
12
|
+
import { enrich } from "../semantic-engine/index.js";
|
|
13
|
+
import { DIAGNOSTICS_FILE_VERSION, DiagnosticCollector, hasErrors } from "../shared/index.js";
|
|
14
|
+
import { renderReport } from "./report.js";
|
|
15
|
+
import { loadCompiled } from "./store.js";
|
|
16
|
+
import { CLI_NAME, CLI_VERSION } from "./version.js";
|
|
17
|
+
const ADAPTERS = {
|
|
18
|
+
react: reactAdapter,
|
|
19
|
+
vue: vueAdapter,
|
|
20
|
+
angular: angularAdapter,
|
|
21
|
+
storybook: storybookAdapter,
|
|
22
|
+
tokens: designTokensAdapter
|
|
23
|
+
};
|
|
24
|
+
export const REVIEW_VERSION = "1.0.0";
|
|
25
|
+
const within = (location, dir) => dir === "." || location === dir || location.startsWith(`${dir}/`);
|
|
26
|
+
/** Limits an adapter to the directories the configuration names for its type. No path means everything. */
|
|
27
|
+
function scoped(adapter, paths) {
|
|
28
|
+
if (!paths)
|
|
29
|
+
return adapter;
|
|
30
|
+
return {
|
|
31
|
+
...adapter,
|
|
32
|
+
discover: async (context) => {
|
|
33
|
+
const found = await adapter.discover(context);
|
|
34
|
+
return { ...found, sources: found.sources.filter((s) => paths.some((p) => within(s.location, p))) };
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function adaptersFor(config) {
|
|
39
|
+
const adapters = [...INVENTORY_ADAPTERS];
|
|
40
|
+
for (const type of Object.keys(ADAPTERS)) {
|
|
41
|
+
const entries = config.sources.filter((s) => s.type === type);
|
|
42
|
+
if (entries.length === 0)
|
|
43
|
+
continue;
|
|
44
|
+
const unrestricted = entries.some((e) => e.path === undefined);
|
|
45
|
+
adapters.push(scoped(ADAPTERS[type], unrestricted ? undefined : entries.flatMap((e) => (e.path === undefined ? [] : [e.path]))));
|
|
46
|
+
}
|
|
47
|
+
return adapters;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Adds pattern definitions and approved policies, then applies people's decisions, from the human-written files in
|
|
51
|
+
* the project. Patterns come first so that a policy can name them.
|
|
52
|
+
*/
|
|
53
|
+
function applyGovernance(cwd, config, draft, clock) {
|
|
54
|
+
const diagnostics = [];
|
|
55
|
+
const summary = {
|
|
56
|
+
patterns: 0,
|
|
57
|
+
patternsSkipped: 0,
|
|
58
|
+
patternsAwaitingApproval: 0,
|
|
59
|
+
policies: 0,
|
|
60
|
+
decisionsApplied: 0,
|
|
61
|
+
decisionsStale: 0
|
|
62
|
+
};
|
|
63
|
+
const evidence = [];
|
|
64
|
+
const read = (rel) => {
|
|
65
|
+
const file = path.resolve(cwd, rel);
|
|
66
|
+
return existsSync(file) ? readFileSync(file, "utf8") : undefined;
|
|
67
|
+
};
|
|
68
|
+
const bad = (message) => {
|
|
69
|
+
diagnostics.push({ code: "governance.invalid-file", severity: "error", message });
|
|
70
|
+
};
|
|
71
|
+
let contract = draft;
|
|
72
|
+
const patternsFile = config.governance?.patternsFile ?? PATTERNS_FILE;
|
|
73
|
+
const patternsText = read(patternsFile);
|
|
74
|
+
if (patternsText !== undefined) {
|
|
75
|
+
const parsed = parsePatterns(patternsText, patternsFile);
|
|
76
|
+
if (parsed.ok) {
|
|
77
|
+
const compiled = compilePatterns(contract, parsed.value, {
|
|
78
|
+
file: patternsFile,
|
|
79
|
+
text: patternsText,
|
|
80
|
+
clock
|
|
81
|
+
});
|
|
82
|
+
contract = compiled.contract;
|
|
83
|
+
evidence.push(...compiled.evidence);
|
|
84
|
+
diagnostics.push(...compiled.diagnostics);
|
|
85
|
+
summary.patterns = compiled.summary.added;
|
|
86
|
+
summary.patternsSkipped = compiled.summary.skipped;
|
|
87
|
+
summary.patternsAwaitingApproval = compiled.summary.awaitingApproval;
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
bad(parsed.message);
|
|
91
|
+
}
|
|
92
|
+
const policiesFile = config.governance?.policiesFile ?? POLICIES_FILE;
|
|
93
|
+
const policiesText = read(policiesFile);
|
|
94
|
+
if (policiesText !== undefined) {
|
|
95
|
+
const parsed = parsePolicies(policiesText);
|
|
96
|
+
if (parsed.ok) {
|
|
97
|
+
const compiled = compilePolicies(contract, parsed.value, policiesFile);
|
|
98
|
+
contract = compiled.contract;
|
|
99
|
+
diagnostics.push(...compiled.diagnostics);
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
bad(parsed.message);
|
|
103
|
+
}
|
|
104
|
+
const decisionsText = read(config.governance?.decisionsFile ?? DECISIONS_FILE);
|
|
105
|
+
if (decisionsText !== undefined) {
|
|
106
|
+
const parsed = parseDecisions(decisionsText);
|
|
107
|
+
if (parsed.ok) {
|
|
108
|
+
const result = applyDecisions(contract, parsed.value, { approvers: config.governance?.approvers });
|
|
109
|
+
contract = result.contract;
|
|
110
|
+
diagnostics.push(...result.diagnostics);
|
|
111
|
+
summary.decisionsApplied = result.applied.length;
|
|
112
|
+
summary.decisionsStale = result.stale.length;
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
bad(parsed.message);
|
|
116
|
+
}
|
|
117
|
+
summary.policies = contract.policies.length - draft.policies.length;
|
|
118
|
+
return { contract, evidence, diagnostics, summary };
|
|
119
|
+
}
|
|
120
|
+
function mapSources(config, compiled) {
|
|
121
|
+
return config.sources.map((entry) => {
|
|
122
|
+
const sources = compiled.irDocuments
|
|
123
|
+
.filter((d) => d.source.type === entry.type || (entry.type === "tokens" && d.source.type === "tokens"))
|
|
124
|
+
.filter((d) => entry.path === undefined || within(d.source.location, entry.path))
|
|
125
|
+
.map((d) => d.source.id)
|
|
126
|
+
.sort();
|
|
127
|
+
return { configId: entry.id, type: entry.type, ...(entry.path ? { path: entry.path } : {}), sources };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** discover, extract, reconcile, enrich, verify and render. Reads the project; writes nothing. */
|
|
131
|
+
export async function runPipeline(input) {
|
|
132
|
+
const { config, clock } = input;
|
|
133
|
+
const declarationRoots = config.sources.flatMap((s) => (s.declarations && s.path ? [s.path] : []));
|
|
134
|
+
const declaredFrameworks = Object.fromEntries(config.sources.flatMap((s) => s.declarations && s.path && (s.type === "react" || s.type === "vue" || s.type === "angular")
|
|
135
|
+
? [[s.path, s.type]]
|
|
136
|
+
: []));
|
|
137
|
+
const compiled = await compile({
|
|
138
|
+
fs: nodeFs(input.cwd),
|
|
139
|
+
adapters: adaptersFor(config),
|
|
140
|
+
clock,
|
|
141
|
+
...(declarationRoots.length > 0 ? { discovery: { declarationRoots, declaredFrameworks } } : {})
|
|
142
|
+
});
|
|
143
|
+
const reconciled = reconcile(compiled.irDocuments, {
|
|
144
|
+
name: config.name,
|
|
145
|
+
...(config.contractId ? { contractId: config.contractId } : {}),
|
|
146
|
+
...(config.description ? { description: config.description } : {}),
|
|
147
|
+
compilerVersion: CLI_VERSION,
|
|
148
|
+
clock
|
|
149
|
+
});
|
|
150
|
+
const enriched = enrich({
|
|
151
|
+
contract: reconciled.contract,
|
|
152
|
+
evidence: reconciled.evidence,
|
|
153
|
+
docs: compiled.irDocuments
|
|
154
|
+
});
|
|
155
|
+
const governed = applyGovernance(input.cwd, config, enriched.contract, clock);
|
|
156
|
+
const final = finalizeAndVerify(governed.contract, [...enriched.evidence, ...governed.evidence]);
|
|
157
|
+
const review = buildReviewQueue(governed.contract);
|
|
158
|
+
const mapping = mapSources(config, compiled);
|
|
159
|
+
const collector = new DiagnosticCollector();
|
|
160
|
+
collector.addAll(compiled.diagnostics);
|
|
161
|
+
collector.addAll(reconciled.diagnostics);
|
|
162
|
+
collector.addAll(enriched.diagnostics);
|
|
163
|
+
collector.addAll(governed.diagnostics);
|
|
164
|
+
collector.addAll(final.diagnostics);
|
|
165
|
+
for (const m of mapping)
|
|
166
|
+
if (m.sources.length === 0)
|
|
167
|
+
collector.warning("cli.source-matched-nothing", `configured source "${m.configId}" (${m.type}${m.path ? ` at ${m.path}` : ""}) matched no source`);
|
|
168
|
+
const diagnostics = collector.toArray();
|
|
169
|
+
const entries = [
|
|
170
|
+
{ path: "invariant.contract.json", value: final.contract },
|
|
171
|
+
{ path: "invariant.evidence.json", value: final.evidenceFile },
|
|
172
|
+
{
|
|
173
|
+
path: "invariant.diagnostics.json",
|
|
174
|
+
value: { diagnosticsVersion: DIAGNOSTICS_FILE_VERSION, diagnostics }
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: "invariant.review.json",
|
|
178
|
+
value: { reviewVersion: REVIEW_VERSION, items: review }
|
|
179
|
+
},
|
|
180
|
+
{ path: "invariant.report.html", text: renderReport({ contract: final.contract, review, diagnostics }) }
|
|
181
|
+
];
|
|
182
|
+
if (input.keepIr)
|
|
183
|
+
entries.push(...toArtifactEntries(compiled).filter((e) => e.path.startsWith("ir/") || e.path === "invariant.project.json"));
|
|
184
|
+
const artifacts = renderArtifacts(entries, {
|
|
185
|
+
tool: { name: CLI_NAME, version: CLI_VERSION },
|
|
186
|
+
generatedAt: clock(),
|
|
187
|
+
sources: compiled.sources
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
compiled,
|
|
191
|
+
reconciled,
|
|
192
|
+
final,
|
|
193
|
+
review,
|
|
194
|
+
diagnostics,
|
|
195
|
+
mapping,
|
|
196
|
+
governance: governed.summary,
|
|
197
|
+
ok: !hasErrors(diagnostics),
|
|
198
|
+
artifacts
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Compiles a project that uses a design system it does not contain. The contract the library ships is the base:
|
|
203
|
+
* its components, tokens and patterns are taken as they are, with their evidence and approvals. This project's own
|
|
204
|
+
* pattern definitions, policies and decisions are applied on top, the result is verified and written locally. Nothing is
|
|
205
|
+
* read from sources, so a new version of the library shows up as a stale contract (`verify --fresh`).
|
|
206
|
+
*/
|
|
207
|
+
export function runLayeredPipeline(input) {
|
|
208
|
+
const { config, clock } = input;
|
|
209
|
+
const from = config.extends?.contract;
|
|
210
|
+
if (!from)
|
|
211
|
+
throw new Error("runLayeredPipeline needs extends.contract");
|
|
212
|
+
const base = loadCompiled(path.resolve(input.cwd, from));
|
|
213
|
+
const { integrity: _integrity, ...draft } = base.contract;
|
|
214
|
+
const governed = applyGovernance(input.cwd, config, draft, clock);
|
|
215
|
+
const final = finalizeAndVerify(governed.contract, [...base.evidence.evidence, ...governed.evidence]);
|
|
216
|
+
const review = buildReviewQueue(governed.contract);
|
|
217
|
+
const collector = new DiagnosticCollector();
|
|
218
|
+
collector.addAll(governed.diagnostics);
|
|
219
|
+
collector.addAll(final.diagnostics);
|
|
220
|
+
const diagnostics = collector.toArray();
|
|
221
|
+
const baseHash = base.contract.integrity.contentHash;
|
|
222
|
+
const entries = [
|
|
223
|
+
{ path: "invariant.contract.json", value: final.contract },
|
|
224
|
+
{ path: "invariant.evidence.json", value: final.evidenceFile },
|
|
225
|
+
{
|
|
226
|
+
path: "invariant.diagnostics.json",
|
|
227
|
+
value: { diagnosticsVersion: DIAGNOSTICS_FILE_VERSION, diagnostics }
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
path: "invariant.review.json",
|
|
231
|
+
value: { reviewVersion: REVIEW_VERSION, items: review }
|
|
232
|
+
},
|
|
233
|
+
{ path: "invariant.report.html", text: renderReport({ contract: final.contract, review, diagnostics }) }
|
|
234
|
+
];
|
|
235
|
+
const artifacts = renderArtifacts(entries, {
|
|
236
|
+
tool: { name: CLI_NAME, version: CLI_VERSION },
|
|
237
|
+
generatedAt: clock(),
|
|
238
|
+
sources: [
|
|
239
|
+
{
|
|
240
|
+
id: "extends",
|
|
241
|
+
type: "contract",
|
|
242
|
+
adapterId: "extends",
|
|
243
|
+
adapterVersion: "1",
|
|
244
|
+
inputHash: baseHash,
|
|
245
|
+
irContentHash: baseHash,
|
|
246
|
+
completeness: "complete"
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
final,
|
|
252
|
+
review,
|
|
253
|
+
diagnostics,
|
|
254
|
+
mapping: [
|
|
255
|
+
{ configId: "extends", type: "contract", path: from, sources: [`contract ${baseHash.slice(0, 12)}`] }
|
|
256
|
+
],
|
|
257
|
+
governance: governed.summary,
|
|
258
|
+
ok: !hasErrors(diagnostics),
|
|
259
|
+
artifacts
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CLI_VERSION, NPM_PACKAGE } from "./version.js";
|
|
4
|
+
const posix = (p) => p.split(path.sep).join("/");
|
|
5
|
+
/**
|
|
6
|
+
* Finds the package that ships the design system and says what it needs so that its published tarball carries the
|
|
7
|
+
* compiled contract. A private package is skipped: it is not published, so apps in the same repository use it directly.
|
|
8
|
+
*/
|
|
9
|
+
export function planPublish(cwd, config, outputDirectory) {
|
|
10
|
+
const framework = config.sources.filter((s) => s.type === "react" || s.type === "vue" || s.type === "angular");
|
|
11
|
+
const dirs = [...framework.map((s) => s.path ?? "."), "."];
|
|
12
|
+
for (const dir of dirs) {
|
|
13
|
+
const file = path.join(cwd, dir, "package.json");
|
|
14
|
+
if (!existsSync(file))
|
|
15
|
+
continue;
|
|
16
|
+
let pkg;
|
|
17
|
+
try {
|
|
18
|
+
pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { ok: false, reason: `${posix(path.join(dir, "package.json"))} is not valid JSON` };
|
|
22
|
+
}
|
|
23
|
+
if (typeof pkg.name !== "string" || pkg.name === "")
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
reason: `${posix(path.join(dir, "package.json"))} has no name, so it is not a published package`
|
|
27
|
+
};
|
|
28
|
+
if (pkg.private === true)
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
reason: `${pkg.name} is private, so it is not published (apps in this repository use it directly)`
|
|
32
|
+
};
|
|
33
|
+
const rel = dir === "." ? "." : posix(path.normalize(dir));
|
|
34
|
+
const pkgDirFromRoot = rel;
|
|
35
|
+
const out = posix(path.normalize(outputDirectory));
|
|
36
|
+
const run = `npx ${NPM_PACKAGE}@${CLI_VERSION}`;
|
|
37
|
+
// In the package's own folder the output directory is the folder to ship. Anywhere else it is copied there.
|
|
38
|
+
const toRoot = rel === "." ? "" : ` --cwd ${posix(path.relative(path.join(cwd, rel), cwd)) || "."}`;
|
|
39
|
+
const prepack = rel === "." && out === "invariant"
|
|
40
|
+
? `${run} compile`
|
|
41
|
+
: `${run} compile${toRoot} && ${run} export --to ${posix(path.join(pkgDirFromRoot, "invariant"))} --force${toRoot}`;
|
|
42
|
+
const files = Array.isArray(pkg.files) ? pkg.files : undefined;
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
plan: {
|
|
46
|
+
file,
|
|
47
|
+
name: pkg.name,
|
|
48
|
+
dir: rel,
|
|
49
|
+
prepack,
|
|
50
|
+
hasFiles: files === undefined
|
|
51
|
+
? true
|
|
52
|
+
: files.some((f) => f === "invariant" || f === "invariant/" || f === "invariant/**"),
|
|
53
|
+
hasPrepack: typeof pkg.scripts?.prepack === "string"
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { ok: false, reason: "no package.json with a name was found for the design system" };
|
|
58
|
+
}
|
|
59
|
+
/** Adds `invariant` to `files` (only if the package lists its files) and a `prepack` script (only if it has none). */
|
|
60
|
+
export function applyPublish(plan) {
|
|
61
|
+
const raw = readFileSync(plan.file, "utf8");
|
|
62
|
+
const indent = /^\{\n(\t| +)"/.exec(raw)?.[1] ?? 2;
|
|
63
|
+
const pkg = JSON.parse(raw);
|
|
64
|
+
const done = [];
|
|
65
|
+
if (Array.isArray(pkg.files) && !plan.hasFiles) {
|
|
66
|
+
pkg.files = [...pkg.files, "invariant"];
|
|
67
|
+
done.push('added "invariant" to files');
|
|
68
|
+
}
|
|
69
|
+
if (!plan.hasPrepack) {
|
|
70
|
+
pkg.scripts = { ...(pkg.scripts ?? {}), prepack: plan.prepack };
|
|
71
|
+
done.push("added a prepack script that compiles the contract");
|
|
72
|
+
}
|
|
73
|
+
if (done.length > 0)
|
|
74
|
+
writeFileSync(plan.file, `${JSON.stringify(pkg, null, indent)}${raw.endsWith("\n") ? "\n" : ""}`);
|
|
75
|
+
return done;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=publish.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const ESCAPES = {
|
|
2
|
+
"&": "&",
|
|
3
|
+
"<": "<",
|
|
4
|
+
">": ">",
|
|
5
|
+
'"': """,
|
|
6
|
+
"'": "'"
|
|
7
|
+
};
|
|
8
|
+
/** Every value that reaches the page goes through here: names and messages come from customer source. */
|
|
9
|
+
export const esc = (value) => String(value).replace(/[&<>"']/g, (c) => ESCAPES[c]);
|
|
10
|
+
export const STYLE = `body{font:14px/1.5 system-ui,sans-serif;margin:2rem auto;max-width:72rem;padding:0 1rem;color:#1a1a1a}
|
|
11
|
+
h1,h2{font-weight:600}table{border-collapse:collapse;width:100%;margin:.5rem 0 1.5rem}
|
|
12
|
+
th,td{border:1px solid #d0d0d0;padding:.3rem .5rem;text-align:left;vertical-align:top}th{background:#f3f3f3}
|
|
13
|
+
code{font:12px ui-monospace,monospace}summary{cursor:pointer;margin:.5rem 0}.error{color:#a40000}.warning{color:#8a5a00}.info{color:#555}`;
|
|
14
|
+
function table(head, rows) {
|
|
15
|
+
if (rows.length === 0)
|
|
16
|
+
return "<p>None.</p>";
|
|
17
|
+
const th = head.map((h) => `<th>${esc(h)}</th>`).join("");
|
|
18
|
+
const body = rows.map((r) => `<tr>${r.map((c) => `<td>${c}</td>`).join("")}</tr>`).join("\n");
|
|
19
|
+
return `<table><thead><tr>${th}</tr></thead><tbody>\n${body}\n</tbody></table>`;
|
|
20
|
+
}
|
|
21
|
+
/** Rows shown before the rest fold into a `<details>` block. The report never drops rows and needs no script. */
|
|
22
|
+
const VISIBLE_ROWS = 200;
|
|
23
|
+
function longTable(head, rows) {
|
|
24
|
+
if (rows.length <= VISIBLE_ROWS)
|
|
25
|
+
return table(head, rows);
|
|
26
|
+
const rest = rows.length - VISIBLE_ROWS;
|
|
27
|
+
return `${table(head, rows.slice(0, VISIBLE_ROWS))}
|
|
28
|
+
<details><summary>Show the remaining ${esc(rest)} rows</summary>
|
|
29
|
+
${table(head, rows.slice(VISIBLE_ROWS))}
|
|
30
|
+
</details>`;
|
|
31
|
+
}
|
|
32
|
+
const anchor = (id) => `c-${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
33
|
+
/**
|
|
34
|
+
* A self-contained, static HTML summary: no scripts, no external requests, no timestamps. It is a view of the
|
|
35
|
+
* contract for people; the contract file stays canonical.
|
|
36
|
+
*/
|
|
37
|
+
export function renderReport(input) {
|
|
38
|
+
const { contract: c, review, diagnostics } = input;
|
|
39
|
+
const inferred = review.filter((r) => r.kind === "inferred-fact").length;
|
|
40
|
+
const openConflicts = c.conflicts.filter((x) => x.resolution.state === "open").length;
|
|
41
|
+
const counts = (sev) => diagnostics.filter((d) => d.severity === sev).length;
|
|
42
|
+
const parts = [];
|
|
43
|
+
parts.push(`<h1>${esc(c.metadata.name)}</h1>`);
|
|
44
|
+
parts.push(`<p>Contract <code>${esc(c.metadata.contractId)}</code> v${esc(c.contractVersion)}, compiler ${esc(c.metadata.compilerVersion)}, content hash <code>${esc(c.integrity.contentHash)}</code>.</p>`);
|
|
45
|
+
parts.push(table([
|
|
46
|
+
"Components",
|
|
47
|
+
"Tokens",
|
|
48
|
+
"Themes",
|
|
49
|
+
"Guidelines",
|
|
50
|
+
"Open conflicts",
|
|
51
|
+
"Inferred facts",
|
|
52
|
+
"Errors",
|
|
53
|
+
"Warnings"
|
|
54
|
+
], [
|
|
55
|
+
[
|
|
56
|
+
c.components.length,
|
|
57
|
+
c.tokens.length,
|
|
58
|
+
c.themes.length,
|
|
59
|
+
c.guidelines.length,
|
|
60
|
+
openConflicts,
|
|
61
|
+
inferred,
|
|
62
|
+
counts("error"),
|
|
63
|
+
counts("warning")
|
|
64
|
+
].map(esc)
|
|
65
|
+
]));
|
|
66
|
+
parts.push("<h2>Sources</h2>");
|
|
67
|
+
parts.push(table(["Id", "Type", "Location", "Health", "Diagnostics"], c.sources.map((s) => [
|
|
68
|
+
esc(s.id),
|
|
69
|
+
esc(s.type),
|
|
70
|
+
esc(s.location),
|
|
71
|
+
esc(s.health.status),
|
|
72
|
+
esc(s.health.diagnosticCount)
|
|
73
|
+
])));
|
|
74
|
+
parts.push("<h2>Conflicts</h2>");
|
|
75
|
+
parts.push(longTable(["Severity", "Type", "Subject", "Path", "Claims", "State"], c.conflicts.map((x) => [
|
|
76
|
+
`<span class="${esc(x.severity)}">${esc(x.severity)}</span>`,
|
|
77
|
+
esc(x.type),
|
|
78
|
+
`<a href="#${esc(anchor(x.subject))}">${esc(x.subject)}</a>`,
|
|
79
|
+
`<code>${esc(x.path ?? "")}</code>`,
|
|
80
|
+
x.claims
|
|
81
|
+
.map((cl) => `${esc(cl.sourceId)}: <code>${esc(cl.value === undefined ? "(absent)" : JSON.stringify(cl.value))}</code> (${esc(cl.status)})`)
|
|
82
|
+
.join("<br>"),
|
|
83
|
+
esc(x.resolution.state)
|
|
84
|
+
])));
|
|
85
|
+
parts.push("<h2>Components</h2>");
|
|
86
|
+
parts.push(longTable(["Id", "Name", "Implementations", "Props", "Events", "Slots", "Variants"], c.components.map((x) => [
|
|
87
|
+
`<a id="${esc(anchor(x.id))}"></a><code>${esc(x.id)}</code>`,
|
|
88
|
+
esc(x.name),
|
|
89
|
+
esc(x.implementations.map((i) => i.framework).join(", ")),
|
|
90
|
+
esc(x.props.length),
|
|
91
|
+
esc(x.events.length),
|
|
92
|
+
esc(x.slots.length),
|
|
93
|
+
esc(x.variants.length)
|
|
94
|
+
])));
|
|
95
|
+
parts.push("<h2>Tokens</h2>");
|
|
96
|
+
parts.push(longTable(["Id", "Category", "Value", "Modes"], c.tokens.map((t) => [
|
|
97
|
+
`<code>${esc(t.id)}</code>`,
|
|
98
|
+
`${esc(t.category)}${t.facts.category?.status === "inferred" ? " (inferred)" : ""}`,
|
|
99
|
+
`<code>${esc("aliasOf" in t.value && t.value.aliasOf ? `-> ${t.value.aliasOf}` : JSON.stringify(t.value.value))}</code>`,
|
|
100
|
+
esc(Object.keys(t.modes ?? {}).join(", "))
|
|
101
|
+
])));
|
|
102
|
+
parts.push("<h2>Needs review</h2>");
|
|
103
|
+
parts.push(longTable(["Priority", "Kind", "Subject", "Path", "Confidence", "Reason"], review.map((r) => [
|
|
104
|
+
esc(r.priority),
|
|
105
|
+
esc(r.kind),
|
|
106
|
+
esc(r.subject),
|
|
107
|
+
`<code>${esc(r.path ?? "")}</code>`,
|
|
108
|
+
esc(r.confidence ?? ""),
|
|
109
|
+
esc(r.reason)
|
|
110
|
+
])));
|
|
111
|
+
parts.push("<h2>Diagnostics</h2>");
|
|
112
|
+
parts.push(longTable(["Severity", "Code", "Message", "Location"], diagnostics.map((d) => [
|
|
113
|
+
`<span class="${esc(d.severity)}">${esc(d.severity)}</span>`,
|
|
114
|
+
`<code>${esc(d.code)}</code>`,
|
|
115
|
+
esc(d.message),
|
|
116
|
+
esc(d.location ? `${d.location.path}${d.location.line ? `:${d.location.line}` : ""}` : "")
|
|
117
|
+
])));
|
|
118
|
+
return `<!doctype html>
|
|
119
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
120
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
|
|
121
|
+
<title>${esc(c.metadata.name)} - Invariant report</title>
|
|
122
|
+
<style>${STYLE}</style></head><body>
|
|
123
|
+
${parts.join("\n")}
|
|
124
|
+
</body></html>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const MAX_RULES = 6;
|
|
2
|
+
const slug = (name) => name
|
|
3
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
6
|
+
.replace(/^-+|-+$/g, "");
|
|
7
|
+
const idOf = (...parts) => parts.join("-").slice(0, 60).replace(/-+$/, "");
|
|
8
|
+
/**
|
|
9
|
+
* Rules that restate what an observed arrangement already does every time, so they are true of the code that was
|
|
10
|
+
* observed and can be checked against the rest: a parent contains the children it always had, and its children come in
|
|
11
|
+
* the order they always did. Optional children, repeated children and parents whose children may come from elsewhere
|
|
12
|
+
* give no rule, because their absence or order was never consistent. All rules are warnings: they annotate, they do not
|
|
13
|
+
* fail a build, until a person raises them.
|
|
14
|
+
*/
|
|
15
|
+
export function starterRules(tree) {
|
|
16
|
+
if (!tree)
|
|
17
|
+
return [];
|
|
18
|
+
const rules = [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const add = (rule) => {
|
|
21
|
+
if (seen.has(rule.id) || rules.length >= MAX_RULES)
|
|
22
|
+
return;
|
|
23
|
+
seen.add(rule.id);
|
|
24
|
+
rules.push(rule);
|
|
25
|
+
};
|
|
26
|
+
const visit = (node) => {
|
|
27
|
+
if (!node.opaque) {
|
|
28
|
+
const fixed = node.children.filter((c) => !c.optional);
|
|
29
|
+
for (const child of fixed)
|
|
30
|
+
add({
|
|
31
|
+
id: idOf(slug(node.name), "has", slug(child.name)),
|
|
32
|
+
kind: "required-child",
|
|
33
|
+
description: `A ${node.name} contains a ${child.name}.`,
|
|
34
|
+
details: { parent: node.id, child: child.id },
|
|
35
|
+
enforce: { severity: "warning", enforcement: "warn" }
|
|
36
|
+
});
|
|
37
|
+
for (let i = 0; i + 1 < fixed.length; i++) {
|
|
38
|
+
const before = fixed[i];
|
|
39
|
+
const after = fixed[i + 1];
|
|
40
|
+
if (before.id === after.id)
|
|
41
|
+
continue;
|
|
42
|
+
add({
|
|
43
|
+
id: idOf(slug(before.name), "before", slug(after.name)),
|
|
44
|
+
kind: "ordering",
|
|
45
|
+
description: `In a ${node.name}, ${before.name} comes before ${after.name}.`,
|
|
46
|
+
details: { before: before.id, after: after.id, within: node.id },
|
|
47
|
+
enforce: { severity: "warning", enforcement: "warn" }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const child of node.children)
|
|
52
|
+
visit(child);
|
|
53
|
+
};
|
|
54
|
+
visit(tree);
|
|
55
|
+
return rules;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=starter-rules.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { manifestSchema } from "../artifact-generator/index.js";
|
|
4
|
+
import { evidenceFileSchema, parseContract, verifyContract } from "../contract/index.js";
|
|
5
|
+
import { DiagnosticCollector, sha256Hex } from "../shared/index.js";
|
|
6
|
+
import { CliError } from "./errors.js";
|
|
7
|
+
function readText(file, what) {
|
|
8
|
+
if (!existsSync(file))
|
|
9
|
+
throw new CliError(`${what} not found at ${file}. Run \`invariant compile\` first.`);
|
|
10
|
+
return readFileSync(file, "utf8");
|
|
11
|
+
}
|
|
12
|
+
/** Loads the compiled contract and evidence from an output directory, checking structure only. */
|
|
13
|
+
export function loadCompiled(dir) {
|
|
14
|
+
const contractText = readText(path.join(dir, "invariant.contract.json"), "contract");
|
|
15
|
+
const evidenceText = readText(path.join(dir, "invariant.evidence.json"), "evidence file");
|
|
16
|
+
let contractRaw;
|
|
17
|
+
let evidenceRaw;
|
|
18
|
+
try {
|
|
19
|
+
contractRaw = JSON.parse(contractText);
|
|
20
|
+
evidenceRaw = JSON.parse(evidenceText);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new CliError(`output in ${dir} is not valid JSON: ${error.message}`, 1);
|
|
24
|
+
}
|
|
25
|
+
const parsed = parseContract(contractRaw);
|
|
26
|
+
if (!parsed.ok)
|
|
27
|
+
throw new CliError(`invariant.contract.json is invalid: ${parsed.diagnostics[0]?.message ?? "schema error"}`, 1);
|
|
28
|
+
const evidence = evidenceFileSchema.safeParse(evidenceRaw);
|
|
29
|
+
if (!evidence.success)
|
|
30
|
+
throw new CliError(`invariant.evidence.json is invalid: ${evidence.error.issues[0]?.message ?? "schema error"}`, 1);
|
|
31
|
+
return { dir, contract: parsed.value, evidence: evidence.data };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Everything that can be checked without recompiling: the contract's own rules with its evidence, and the
|
|
35
|
+
* manifest's hashes against the files on disk.
|
|
36
|
+
*/
|
|
37
|
+
export function checkOutput(dir) {
|
|
38
|
+
const compiled = loadCompiled(dir);
|
|
39
|
+
const collector = new DiagnosticCollector();
|
|
40
|
+
collector.addAll(verifyContract(compiled.contract, compiled.evidence));
|
|
41
|
+
const manifestFile = path.join(dir, "invariant.manifest.json");
|
|
42
|
+
if (!existsSync(manifestFile)) {
|
|
43
|
+
collector.warning("verify.manifest-missing", "invariant.manifest.json is missing, so file hashes were not checked");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const manifest = manifestSchema.safeParse(JSON.parse(readFileSync(manifestFile, "utf8")));
|
|
47
|
+
if (!manifest.success)
|
|
48
|
+
collector.error("verify.manifest-invalid", "invariant.manifest.json does not match its schema");
|
|
49
|
+
else
|
|
50
|
+
for (const a of manifest.data.artifacts) {
|
|
51
|
+
const file = path.join(dir, a.path);
|
|
52
|
+
if (!existsSync(file))
|
|
53
|
+
collector.error("verify.artifact-missing", `${a.path} is listed in the manifest but missing`);
|
|
54
|
+
else if (sha256Hex(readFileSync(file, "utf8")) !== a.sha256)
|
|
55
|
+
collector.error("verify.artifact-modified", `${a.path} does not match its hash in the manifest`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { compiled, diagnostics: collector.toArray() };
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const CLI_NAME = "invariant";
|
|
2
|
+
export const CLI_VERSION = "0.2.0"; // x-release-please-version
|
|
3
|
+
/** The name this is published under on npm (`invariant` belongs to an unrelated package). */
|
|
4
|
+
export const NPM_PACKAGE = "@invariant-ui/cli";
|
|
5
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { watch } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/** Directories whose contents are never sources. The output directory is added by the caller. */
|
|
4
|
+
const NEVER = new Set(["node_modules", ".git", ".hg", ".svn", ".turbo", ".next", "coverage"]);
|
|
5
|
+
/** True when a project-relative path is inside the output directory or a directory that is never a source. */
|
|
6
|
+
export function isIgnored(rel, outputRel) {
|
|
7
|
+
const parts = rel.split(path.sep).filter(Boolean);
|
|
8
|
+
if (parts.some((p) => NEVER.has(p)))
|
|
9
|
+
return true;
|
|
10
|
+
if (outputRel && outputRel !== ".") {
|
|
11
|
+
const out = outputRel.split(path.sep).filter(Boolean);
|
|
12
|
+
if (out.every((seg, i) => parts[i] === seg))
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Watches a project and calls `onChange` after changes settle. Changes that arrive while `onChange` is running
|
|
19
|
+
* are collected and trigger exactly one more run, so a burst of saves never queues many compiles.
|
|
20
|
+
* Resolves when `signal` aborts.
|
|
21
|
+
*/
|
|
22
|
+
export async function watchProject(options) {
|
|
23
|
+
const { root, signal, debounceMs = 250 } = options;
|
|
24
|
+
const outputRel = path.isAbsolute(options.outputDir)
|
|
25
|
+
? path.relative(root, options.outputDir)
|
|
26
|
+
: path.normalize(options.outputDir);
|
|
27
|
+
const pending = new Set();
|
|
28
|
+
let timer;
|
|
29
|
+
let running = false;
|
|
30
|
+
const flush = async () => {
|
|
31
|
+
timer = undefined;
|
|
32
|
+
if (running || pending.size === 0)
|
|
33
|
+
return;
|
|
34
|
+
running = true;
|
|
35
|
+
try {
|
|
36
|
+
while (pending.size > 0) {
|
|
37
|
+
const files = [...pending].sort();
|
|
38
|
+
pending.clear();
|
|
39
|
+
await options.onChange(files);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
running = false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
const events = watch(root, { recursive: true, ...(signal ? { signal } : {}) });
|
|
48
|
+
options.onReady?.();
|
|
49
|
+
for await (const event of events) {
|
|
50
|
+
const rel = event.filename ? path.normalize(event.filename.toString()) : "";
|
|
51
|
+
if (!rel || isIgnored(rel, outputRel))
|
|
52
|
+
continue;
|
|
53
|
+
pending.add(rel);
|
|
54
|
+
if (timer)
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
timer = setTimeout(() => void flush(), debounceMs);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.name !== "AbortError")
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (timer)
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=watch.js.map
|