@invariant-ui/cli 0.0.0-stage → 0.1.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 +248 -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/ast.js +105 -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/program.js +58 -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-storybook/static-eval.js +145 -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/scan.js +164 -0
- package/dist/contract-query/search.js +132 -0
- package/dist/contract-query/system.js +148 -0
- package/dist/contract-query/validate-code.js +239 -0
- package/dist/contract-query/validate-usage.js +150 -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,148 @@
|
|
|
1
|
+
import { globToRegExp, matchesAny } from "../glob.js";
|
|
2
|
+
import { cmp, hashText, lineOfJsonMember, posix, readBounded } from "./support.js";
|
|
3
|
+
const DEP_KINDS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
4
|
+
export async function discoverPackages(env) {
|
|
5
|
+
const workspaces = await discoverWorkspaces(env);
|
|
6
|
+
const manifestPaths = env.files.filter((f) => posix.basename(f) === "package.json");
|
|
7
|
+
const positive = workspaces.patterns
|
|
8
|
+
.filter((p) => !p.startsWith("!"))
|
|
9
|
+
.map((p) => globToRegExp(trimSlashes(p)));
|
|
10
|
+
const negative = workspaces.patterns
|
|
11
|
+
.filter((p) => p.startsWith("!"))
|
|
12
|
+
.map((p) => globToRegExp(trimSlashes(p.slice(1))));
|
|
13
|
+
const selected = manifestPaths.filter((manifest) => {
|
|
14
|
+
const dir = posix.dirname(manifest);
|
|
15
|
+
if (dir === ".")
|
|
16
|
+
return true;
|
|
17
|
+
if (env.declarationRoots?.has(dir))
|
|
18
|
+
return true;
|
|
19
|
+
// An ng-packagr library root (a package.json next to an ng-package.json) is a package even when the
|
|
20
|
+
// repository declares no workspaces: its source lives in a subfolder such as `src/`.
|
|
21
|
+
if (env.fileSet.has(posix.join(dir, "ng-package.json")))
|
|
22
|
+
return true;
|
|
23
|
+
if (workspaces.kind === "none")
|
|
24
|
+
return false;
|
|
25
|
+
return matchesAny(dir, positive) && !matchesAny(dir, negative);
|
|
26
|
+
});
|
|
27
|
+
// Without workspace config and without a root manifest, fall back to every manifest found.
|
|
28
|
+
const chosen = selected.length === 0 && workspaces.kind === "none" ? manifestPaths : selected;
|
|
29
|
+
const packages = [];
|
|
30
|
+
for (const manifestPath of chosen) {
|
|
31
|
+
const text = await readBounded(env, manifestPath);
|
|
32
|
+
if (text === undefined)
|
|
33
|
+
continue;
|
|
34
|
+
let manifest;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(text);
|
|
37
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
38
|
+
throw new Error("not an object");
|
|
39
|
+
manifest = parsed;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
env.diagnostics.error("discovery.invalid-manifest", `Invalid ${manifestPath}: ${error.message}`, {
|
|
43
|
+
location: { path: manifestPath }
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
packages.push({ info: toPackageInfo(manifestPath, manifest, text), manifest, manifestText: text });
|
|
48
|
+
}
|
|
49
|
+
if (packages.length === 0) {
|
|
50
|
+
env.diagnostics.warning("discovery.no-package", "No package.json was found; package, framework and entry-point discovery are empty");
|
|
51
|
+
}
|
|
52
|
+
packages.sort((a, b) => cmp(a.info.dir, b.info.dir));
|
|
53
|
+
// Two packages can share a name (a repository root and the library it contains): ids stay unique.
|
|
54
|
+
const seenIds = new Set();
|
|
55
|
+
for (const raw of packages) {
|
|
56
|
+
if (seenIds.has(raw.info.id))
|
|
57
|
+
raw.info.id = `package:${raw.info.name ?? ""}@${raw.info.dir}`;
|
|
58
|
+
seenIds.add(raw.info.id);
|
|
59
|
+
}
|
|
60
|
+
return { packages, workspaces };
|
|
61
|
+
}
|
|
62
|
+
async function discoverWorkspaces(env) {
|
|
63
|
+
if (env.fileSet.has("pnpm-workspace.yaml")) {
|
|
64
|
+
const text = await readBounded(env, "pnpm-workspace.yaml");
|
|
65
|
+
if (text !== undefined)
|
|
66
|
+
return {
|
|
67
|
+
kind: "pnpm-workspace",
|
|
68
|
+
patterns: parsePnpmWorkspacePackages(text),
|
|
69
|
+
source: "pnpm-workspace.yaml"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (env.fileSet.has("package.json")) {
|
|
73
|
+
const text = await readBounded(env, "package.json");
|
|
74
|
+
try {
|
|
75
|
+
const manifest = JSON.parse(text ?? "{}");
|
|
76
|
+
const raw = manifest.workspaces;
|
|
77
|
+
const list = Array.isArray(raw)
|
|
78
|
+
? raw
|
|
79
|
+
: isRecord(raw) && Array.isArray(raw.packages)
|
|
80
|
+
? raw.packages
|
|
81
|
+
: undefined;
|
|
82
|
+
if (list)
|
|
83
|
+
return {
|
|
84
|
+
kind: "package-json",
|
|
85
|
+
patterns: list.filter((p) => typeof p === "string"),
|
|
86
|
+
source: "package.json"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Reported when the manifest itself is parsed.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { kind: "none", patterns: [] };
|
|
94
|
+
}
|
|
95
|
+
/** Reads the `packages:` list from pnpm-workspace.yaml without a YAML dependency (block lists only). */
|
|
96
|
+
export function parsePnpmWorkspacePackages(text) {
|
|
97
|
+
const patterns = [];
|
|
98
|
+
let inPackages = false;
|
|
99
|
+
for (const raw of text.split("\n")) {
|
|
100
|
+
const line = raw.replace(/\s+#.*$/, "").replace(/\r$/, "");
|
|
101
|
+
if (/^packages\s*:/.test(line)) {
|
|
102
|
+
inPackages = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (inPackages) {
|
|
106
|
+
const item = /^\s*-\s*(.+?)\s*$/.exec(line);
|
|
107
|
+
if (item)
|
|
108
|
+
patterns.push(item[1].replace(/^["']|["']$/g, ""));
|
|
109
|
+
else if (/^\S/.test(line))
|
|
110
|
+
inPackages = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return patterns;
|
|
114
|
+
}
|
|
115
|
+
function toPackageInfo(manifestPath, manifest, text) {
|
|
116
|
+
const dir = posix.dirname(manifestPath);
|
|
117
|
+
const name = typeof manifest.name === "string" && manifest.name ? manifest.name : undefined;
|
|
118
|
+
const version = typeof manifest.version === "string" && manifest.version ? manifest.version : undefined;
|
|
119
|
+
const dependencies = DEP_KINDS.flatMap((kind) => {
|
|
120
|
+
const group = manifest[kind];
|
|
121
|
+
if (!isRecord(group))
|
|
122
|
+
return [];
|
|
123
|
+
return Object.entries(group).map(([depName, range]) => ({ name: depName, range: String(range), kind }));
|
|
124
|
+
}).sort((a, b) => cmp(a.name, b.name) || cmp(a.kind, b.kind));
|
|
125
|
+
return {
|
|
126
|
+
id: `package:${name ?? dir}`,
|
|
127
|
+
...(name ? { name } : {}),
|
|
128
|
+
...(version ? { version } : {}),
|
|
129
|
+
dir,
|
|
130
|
+
private: manifest.private === true,
|
|
131
|
+
manifest: { path: manifestPath, contentHash: hashText(text) },
|
|
132
|
+
dependencies,
|
|
133
|
+
scripts: isRecord(manifest.scripts) ? Object.keys(manifest.scripts).sort() : [],
|
|
134
|
+
frameworks: [],
|
|
135
|
+
buildTools: [],
|
|
136
|
+
entryPoints: []
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function dependencyLine(raw, group, name) {
|
|
140
|
+
return lineOfJsonMember(raw.manifestText, group, name);
|
|
141
|
+
}
|
|
142
|
+
function trimSlashes(p) {
|
|
143
|
+
return p.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
144
|
+
}
|
|
145
|
+
export function isRecord(value) {
|
|
146
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=packages.js.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { relativePathSchema } from "../../contract/index.js";
|
|
2
|
+
import { diagnosticSchema } from "../../shared/index.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const DISCOVERY_VERSION = "1.0.0";
|
|
5
|
+
const hash = z.string().regex(/^[0-9a-f]{64}$/);
|
|
6
|
+
const fileRef = z.strictObject({ path: relativePathSchema, contentHash: hash });
|
|
7
|
+
const dependencyKind = z.enum(["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]);
|
|
8
|
+
export const exportInfoSchema = z.strictObject({
|
|
9
|
+
name: z.string().min(1),
|
|
10
|
+
kind: z.enum(["value", "type", "namespace", "default", "unknown"]),
|
|
11
|
+
/** True when the export has no runtime value: `export type`, interfaces and type aliases. */
|
|
12
|
+
typeOnly: z.boolean(),
|
|
13
|
+
file: relativePathSchema,
|
|
14
|
+
line: z.number().int().positive(),
|
|
15
|
+
reexportOf: z.strictObject({ module: z.string().min(1), external: z.boolean() }).optional()
|
|
16
|
+
});
|
|
17
|
+
export const entryPointSchema = z.strictObject({
|
|
18
|
+
subpath: z.string().min(1),
|
|
19
|
+
condition: z.string().min(1),
|
|
20
|
+
target: z.string().min(1),
|
|
21
|
+
resolution: z.enum(["declared", "mapped", "conventional", "unresolved"]),
|
|
22
|
+
file: relativePathSchema.optional(),
|
|
23
|
+
exports: z.array(exportInfoSchema),
|
|
24
|
+
/** False when exports could not be fully enumerated (external `export *`, parse failure, or limits). */
|
|
25
|
+
exportsComplete: z.boolean(),
|
|
26
|
+
parsedFiles: z.array(fileRef)
|
|
27
|
+
});
|
|
28
|
+
export const frameworkUseSchema = z.strictObject({
|
|
29
|
+
name: z.enum(["react", "angular", "vue"]),
|
|
30
|
+
package: z.string().min(1),
|
|
31
|
+
dependencyKind,
|
|
32
|
+
declaredRange: z.string().min(1),
|
|
33
|
+
installedVersion: z.string().min(1).optional(),
|
|
34
|
+
line: z.number().int().positive().optional()
|
|
35
|
+
});
|
|
36
|
+
export const buildToolSchema = z.strictObject({
|
|
37
|
+
name: z.string().min(1),
|
|
38
|
+
evidence: z.array(z.strictObject({ type: z.enum(["dependency", "config-file"]), ref: z.string().min(1) }))
|
|
39
|
+
});
|
|
40
|
+
export const packageSchema = z.strictObject({
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
name: z.string().min(1).optional(),
|
|
43
|
+
version: z.string().min(1).optional(),
|
|
44
|
+
dir: relativePathSchema,
|
|
45
|
+
private: z.boolean(),
|
|
46
|
+
manifest: fileRef,
|
|
47
|
+
dependencies: z.array(z.strictObject({ name: z.string().min(1), range: z.string(), kind: dependencyKind })),
|
|
48
|
+
scripts: z.array(z.string()),
|
|
49
|
+
frameworks: z.array(frameworkUseSchema),
|
|
50
|
+
buildTools: z.array(buildToolSchema),
|
|
51
|
+
entryPoints: z.array(entryPointSchema)
|
|
52
|
+
});
|
|
53
|
+
export const packageManagerSchema = z.strictObject({
|
|
54
|
+
name: z.enum(["npm", "pnpm", "yarn", "bun", "unknown"]),
|
|
55
|
+
version: z.string().min(1).optional(),
|
|
56
|
+
evidence: z.array(z.string().min(1))
|
|
57
|
+
});
|
|
58
|
+
export const workspacesSchema = z.strictObject({
|
|
59
|
+
kind: z.enum(["pnpm-workspace", "package-json", "none"]),
|
|
60
|
+
patterns: z.array(z.string()),
|
|
61
|
+
source: relativePathSchema.optional()
|
|
62
|
+
});
|
|
63
|
+
export const storybookSchema = z.strictObject({
|
|
64
|
+
id: z.string().min(1),
|
|
65
|
+
configPath: relativePathSchema,
|
|
66
|
+
configHash: hash,
|
|
67
|
+
directory: relativePathSchema,
|
|
68
|
+
package: z.string().min(1).optional(),
|
|
69
|
+
framework: z.string().min(1).optional(),
|
|
70
|
+
addons: z.array(z.string()),
|
|
71
|
+
stories: z.array(z.strictObject({
|
|
72
|
+
pattern: z.string().optional(),
|
|
73
|
+
directory: z.string().optional(),
|
|
74
|
+
files: z.string().optional(),
|
|
75
|
+
titlePrefix: z.string().optional()
|
|
76
|
+
})),
|
|
77
|
+
staticDirs: z.array(z.string()),
|
|
78
|
+
/** Config properties whose value could not be determined statically. */
|
|
79
|
+
dynamicProperties: z.array(z.string()),
|
|
80
|
+
version: z
|
|
81
|
+
.strictObject({ package: z.string(), declaredRange: z.string(), installedVersion: z.string().optional() })
|
|
82
|
+
.optional(),
|
|
83
|
+
builtOutput: z.strictObject({ path: relativePathSchema, hasIndexJson: z.boolean() }).optional(),
|
|
84
|
+
storyFiles: z.array(relativePathSchema),
|
|
85
|
+
storyFileCount: z.number().int().nonnegative(),
|
|
86
|
+
storyFilesTruncated: z.boolean()
|
|
87
|
+
});
|
|
88
|
+
export const cssVariableSchema = z.strictObject({
|
|
89
|
+
name: z.string().min(1),
|
|
90
|
+
scope: z.string(),
|
|
91
|
+
line: z.number().int().positive(),
|
|
92
|
+
value: z.string().max(200),
|
|
93
|
+
aliasOf: z.string().optional()
|
|
94
|
+
});
|
|
95
|
+
export const tokenFileSchema = z.strictObject({
|
|
96
|
+
path: relativePathSchema,
|
|
97
|
+
kind: z.enum(["css-custom-properties", "json-tokens", "style-dictionary-config", "tailwind-config"]),
|
|
98
|
+
/** Shape heuristic only: it says what the file looks like, not that it conforms to a spec. */
|
|
99
|
+
format: z.enum(["dtcg-like", "style-dictionary-like", "unknown"]).optional(),
|
|
100
|
+
contentHash: hash,
|
|
101
|
+
package: z.string().min(1).optional(),
|
|
102
|
+
cssVariables: z.array(cssVariableSchema).optional(),
|
|
103
|
+
cssVariablesTruncated: z.boolean().optional(),
|
|
104
|
+
tokenCount: z.number().int().nonnegative().optional()
|
|
105
|
+
});
|
|
106
|
+
export const docFileSchema = z.strictObject({
|
|
107
|
+
path: relativePathSchema,
|
|
108
|
+
kind: z.enum(["readme", "changelog", "mdx", "markdown"]),
|
|
109
|
+
title: z.string().optional(),
|
|
110
|
+
headings: z.array(z.strictObject({
|
|
111
|
+
level: z.number().int().min(1).max(6),
|
|
112
|
+
text: z.string(),
|
|
113
|
+
line: z.number().int().positive()
|
|
114
|
+
})),
|
|
115
|
+
lineCount: z.number().int().nonnegative(),
|
|
116
|
+
contentHash: hash,
|
|
117
|
+
package: z.string().min(1).optional()
|
|
118
|
+
});
|
|
119
|
+
export const graphSchema = z.strictObject({
|
|
120
|
+
nodes: z.array(z.strictObject({
|
|
121
|
+
id: z.string().min(1),
|
|
122
|
+
type: z.enum(["package", "framework", "build-tool", "storybook-config", "token-file", "doc-file"]),
|
|
123
|
+
label: z.string().min(1)
|
|
124
|
+
})),
|
|
125
|
+
edges: z.array(z.strictObject({
|
|
126
|
+
from: z.string().min(1),
|
|
127
|
+
to: z.string().min(1),
|
|
128
|
+
type: z.enum(["depends-on", "uses-framework", "built-with", "contains", "configured-by", "documents"]),
|
|
129
|
+
weight: z.number().int().positive().optional()
|
|
130
|
+
}))
|
|
131
|
+
});
|
|
132
|
+
export const projectDiscoverySchema = z.strictObject({
|
|
133
|
+
discoveryVersion: z.literal(DISCOVERY_VERSION),
|
|
134
|
+
packageManager: packageManagerSchema,
|
|
135
|
+
workspaces: workspacesSchema,
|
|
136
|
+
packages: z.array(packageSchema),
|
|
137
|
+
storybooks: z.array(storybookSchema),
|
|
138
|
+
tokenFiles: z.array(tokenFileSchema),
|
|
139
|
+
docFiles: z.array(docFileSchema),
|
|
140
|
+
graph: graphSchema,
|
|
141
|
+
stats: z.strictObject({ filesScanned: z.number().int().nonnegative(), truncated: z.boolean() }),
|
|
142
|
+
diagnostics: z.array(diagnosticSchema)
|
|
143
|
+
});
|
|
144
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { globToRegExp, matchesAny } from "../glob.js";
|
|
3
|
+
import { isRecord } from "./packages.js";
|
|
4
|
+
import { cmp, hashText, owningPackage, posix, readBounded } from "./support.js";
|
|
5
|
+
const CONFIG_NAME = /^main\.[cm]?[jt]s$/;
|
|
6
|
+
/** Storybook configs found under any `.storybook` directory, parsed statically (the config is never executed). */
|
|
7
|
+
export async function discoverStorybooks(env, packages) {
|
|
8
|
+
const configs = env.files.filter((f) => posix.basename(posix.dirname(f)) === ".storybook" && CONFIG_NAME.test(posix.basename(f)));
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const configPath of configs) {
|
|
11
|
+
const text = await readBounded(env, configPath);
|
|
12
|
+
if (text === undefined)
|
|
13
|
+
continue;
|
|
14
|
+
const configDir = posix.dirname(configPath);
|
|
15
|
+
const directory = posix.dirname(configDir);
|
|
16
|
+
const evaluated = evaluateConfig(configPath, text);
|
|
17
|
+
if (!evaluated) {
|
|
18
|
+
env.diagnostics.warning("discovery.storybook-unparsed", `No static config object found in ${configPath}`, { location: { path: configPath } });
|
|
19
|
+
}
|
|
20
|
+
const config = evaluated ?? { values: {}, dynamic: ["<config>"] };
|
|
21
|
+
for (const property of config.dynamic) {
|
|
22
|
+
env.diagnostics.warning("discovery.storybook-dynamic-config", `Storybook '${property}' in ${configPath} is not statically determinable`, {
|
|
23
|
+
location: { path: configPath }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const stories = toStories(config.values.stories);
|
|
27
|
+
const pkg = owningPackage(packages.map((p) => ({ dir: p.info.dir, raw: p })), configPath)?.raw;
|
|
28
|
+
const { files, total } = expandStories(env, configDir, stories);
|
|
29
|
+
out.push({
|
|
30
|
+
id: `storybook:${directory}`,
|
|
31
|
+
configPath,
|
|
32
|
+
configHash: hashText(text),
|
|
33
|
+
directory,
|
|
34
|
+
...(pkg ? { package: pkg.info.id } : {}),
|
|
35
|
+
...frameworkOf(config.values.framework),
|
|
36
|
+
addons: toStrings(config.values.addons).sort(),
|
|
37
|
+
stories,
|
|
38
|
+
staticDirs: toStrings(config.values.staticDirs).sort(),
|
|
39
|
+
dynamicProperties: [...config.dynamic].sort(),
|
|
40
|
+
...(pkg ? await storybookVersion(env, pkg) : {}),
|
|
41
|
+
...(await builtOutput(env, directory)),
|
|
42
|
+
storyFiles: files.slice(0, env.limits.maxStoryFiles),
|
|
43
|
+
storyFileCount: total,
|
|
44
|
+
storyFilesTruncated: total > env.limits.maxStoryFiles
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return out.sort((a, b) => cmp(a.configPath, b.configPath));
|
|
48
|
+
}
|
|
49
|
+
function evaluateConfig(file, text) {
|
|
50
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.ES2022, true, /\.[cm]?js$/.test(file) ? ts.ScriptKind.JS : ts.ScriptKind.TS);
|
|
51
|
+
const bindings = new Map();
|
|
52
|
+
let exported;
|
|
53
|
+
for (const statement of sf.statements) {
|
|
54
|
+
if (ts.isVariableStatement(statement)) {
|
|
55
|
+
for (const d of statement.declarationList.declarations)
|
|
56
|
+
if (ts.isIdentifier(d.name) && d.initializer)
|
|
57
|
+
bindings.set(d.name.text, d.initializer);
|
|
58
|
+
}
|
|
59
|
+
else if (ts.isExportAssignment(statement) && !statement.isExportEquals)
|
|
60
|
+
exported = statement.expression;
|
|
61
|
+
else if (ts.isExpressionStatement(statement) &&
|
|
62
|
+
ts.isBinaryExpression(statement.expression) &&
|
|
63
|
+
statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
64
|
+
statement.expression.left.getText(sf) === "module.exports") {
|
|
65
|
+
exported = statement.expression.right;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const object = unwrap(exported ?? bindings.get("config"), bindings);
|
|
69
|
+
if (!object || !ts.isObjectLiteralExpression(object))
|
|
70
|
+
return undefined;
|
|
71
|
+
const values = {};
|
|
72
|
+
const dynamic = [];
|
|
73
|
+
for (const property of object.properties) {
|
|
74
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
75
|
+
!(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
|
|
76
|
+
dynamic.push(property.name?.getText(sf) ?? "<spread>");
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const name = property.name.text;
|
|
80
|
+
const result = evaluate(property.initializer, bindings);
|
|
81
|
+
values[name] = result.value;
|
|
82
|
+
if (result.dynamic)
|
|
83
|
+
dynamic.push(name);
|
|
84
|
+
}
|
|
85
|
+
return { values, dynamic };
|
|
86
|
+
}
|
|
87
|
+
function unwrap(node, bindings, depth = 0) {
|
|
88
|
+
if (!node || depth > 8)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node))
|
|
91
|
+
return unwrap(node.expression, bindings, depth + 1);
|
|
92
|
+
if (ts.isIdentifier(node))
|
|
93
|
+
return unwrap(bindings.get(node.text), bindings, depth + 1);
|
|
94
|
+
// defineMain({...}) and similar single-argument wrappers
|
|
95
|
+
if (ts.isCallExpression(node) && node.arguments.length === 1)
|
|
96
|
+
return unwrap(node.arguments[0], bindings, depth + 1);
|
|
97
|
+
return node;
|
|
98
|
+
}
|
|
99
|
+
function evaluate(node, bindings, depth = 0) {
|
|
100
|
+
if (depth > 8)
|
|
101
|
+
return { value: undefined, dynamic: true };
|
|
102
|
+
if (ts.isStringLiteralLike(node))
|
|
103
|
+
return { value: node.text, dynamic: false };
|
|
104
|
+
if (ts.isNumericLiteral(node))
|
|
105
|
+
return { value: Number(node.text), dynamic: false };
|
|
106
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
107
|
+
return { value: true, dynamic: false };
|
|
108
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
109
|
+
return { value: false, dynamic: false };
|
|
110
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node))
|
|
111
|
+
return evaluate(node.expression, bindings, depth + 1);
|
|
112
|
+
if (ts.isIdentifier(node) && bindings.has(node.text))
|
|
113
|
+
return evaluate(bindings.get(node.text), bindings, depth + 1);
|
|
114
|
+
// getAbsolutePath("@storybook/addon-docs") is the documented monorepo idiom; its argument is the package name.
|
|
115
|
+
if (ts.isCallExpression(node) &&
|
|
116
|
+
ts.isIdentifier(node.expression) &&
|
|
117
|
+
node.expression.text === "getAbsolutePath") {
|
|
118
|
+
const arg = node.arguments[0];
|
|
119
|
+
if (arg && ts.isStringLiteralLike(arg))
|
|
120
|
+
return { value: arg.text, dynamic: false };
|
|
121
|
+
}
|
|
122
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
123
|
+
let dynamic = false;
|
|
124
|
+
const value = node.elements.map((element) => {
|
|
125
|
+
const inner = evaluate(element, bindings, depth + 1);
|
|
126
|
+
dynamic ||= inner.dynamic;
|
|
127
|
+
return inner.value;
|
|
128
|
+
});
|
|
129
|
+
return { value: value.filter((v) => v !== undefined), dynamic };
|
|
130
|
+
}
|
|
131
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
132
|
+
let dynamic = false;
|
|
133
|
+
const value = {};
|
|
134
|
+
for (const property of node.properties) {
|
|
135
|
+
if (ts.isPropertyAssignment(property) &&
|
|
136
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
|
|
137
|
+
const inner = evaluate(property.initializer, bindings, depth + 1);
|
|
138
|
+
dynamic ||= inner.dynamic;
|
|
139
|
+
if (inner.value !== undefined)
|
|
140
|
+
value[property.name.text] = inner.value;
|
|
141
|
+
}
|
|
142
|
+
else
|
|
143
|
+
dynamic = true;
|
|
144
|
+
}
|
|
145
|
+
return { value, dynamic };
|
|
146
|
+
}
|
|
147
|
+
return { value: undefined, dynamic: true };
|
|
148
|
+
}
|
|
149
|
+
function toStrings(value) {
|
|
150
|
+
if (!Array.isArray(value))
|
|
151
|
+
return [];
|
|
152
|
+
return value.flatMap((item) => typeof item === "string" ? [item] : isRecord(item) && typeof item.name === "string" ? [item.name] : []);
|
|
153
|
+
}
|
|
154
|
+
function toStories(value) {
|
|
155
|
+
if (!Array.isArray(value))
|
|
156
|
+
return [];
|
|
157
|
+
return value.flatMap((item) => {
|
|
158
|
+
if (typeof item === "string")
|
|
159
|
+
return [{ pattern: item }];
|
|
160
|
+
if (isRecord(item)) {
|
|
161
|
+
return [
|
|
162
|
+
{
|
|
163
|
+
...(typeof item.directory === "string" ? { directory: item.directory } : {}),
|
|
164
|
+
...(typeof item.files === "string" ? { files: item.files } : {}),
|
|
165
|
+
...(typeof item.titlePrefix === "string" ? { titlePrefix: item.titlePrefix } : {})
|
|
166
|
+
}
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
return [];
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function frameworkOf(value) {
|
|
173
|
+
if (typeof value === "string")
|
|
174
|
+
return { framework: value };
|
|
175
|
+
if (isRecord(value) && typeof value.name === "string")
|
|
176
|
+
return { framework: value.name };
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
function expandStories(env, configDir, stories) {
|
|
180
|
+
const matchers = [];
|
|
181
|
+
for (const story of stories) {
|
|
182
|
+
const pattern = story.pattern ??
|
|
183
|
+
(story.directory
|
|
184
|
+
? `${story.directory.replace(/\/$/, "")}/${story.files ?? "**/*.stories.@(js|jsx|mjs|ts|tsx)"}`
|
|
185
|
+
: undefined);
|
|
186
|
+
if (!pattern)
|
|
187
|
+
continue;
|
|
188
|
+
const resolved = posix.normalize(posix.join(configDir, pattern));
|
|
189
|
+
if (resolved.startsWith(".."))
|
|
190
|
+
continue;
|
|
191
|
+
matchers.push(globToRegExp(resolved));
|
|
192
|
+
}
|
|
193
|
+
const files = env.files.filter((f) => matchesAny(f, matchers));
|
|
194
|
+
return { files, total: files.length };
|
|
195
|
+
}
|
|
196
|
+
async function storybookVersion(env, pkg) {
|
|
197
|
+
const dep = pkg.info.dependencies.find((d) => d.name === "storybook") ??
|
|
198
|
+
pkg.info.dependencies.find((d) => /^@storybook\/(react|vue3|angular|nextjs|web-components)(-|$)/.test(d.name));
|
|
199
|
+
if (!dep)
|
|
200
|
+
return {};
|
|
201
|
+
let installed;
|
|
202
|
+
for (let dir = pkg.info.dir;; dir = posix.dirname(dir)) {
|
|
203
|
+
const candidate = posix.join(dir, "node_modules", dep.name, "package.json");
|
|
204
|
+
if (await env.fs.exists(candidate)) {
|
|
205
|
+
try {
|
|
206
|
+
const version = JSON.parse(await env.fs.readFile(candidate)).version;
|
|
207
|
+
if (typeof version === "string")
|
|
208
|
+
installed = version;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Unreadable installed manifest: leave installedVersion unknown.
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (dir === ".")
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
version: {
|
|
220
|
+
package: dep.name,
|
|
221
|
+
declaredRange: dep.range,
|
|
222
|
+
...(installed ? { installedVersion: installed } : {})
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function builtOutput(env, directory) {
|
|
227
|
+
const dir = posix.join(directory, "storybook-static");
|
|
228
|
+
if (!(await env.fs.exists(dir)))
|
|
229
|
+
return {};
|
|
230
|
+
return { builtOutput: { path: dir, hasIndexJson: await env.fs.exists(posix.join(dir, "index.json")) } };
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=storybook.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { sha256Hex } from "../../shared/index.js";
|
|
3
|
+
export const DEFAULT_LIMITS = {
|
|
4
|
+
maxFiles: 50_000,
|
|
5
|
+
maxFileBytes: 1_000_000,
|
|
6
|
+
maxParsedFilesPerEntry: 5000,
|
|
7
|
+
maxCssVariablesPerFile: 5000,
|
|
8
|
+
maxStoryFiles: 1000,
|
|
9
|
+
maxHeadings: 100
|
|
10
|
+
};
|
|
11
|
+
export const posix = path.posix;
|
|
12
|
+
export function hashText(text) {
|
|
13
|
+
return sha256Hex(text);
|
|
14
|
+
}
|
|
15
|
+
export function cmp(a, b) {
|
|
16
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
17
|
+
}
|
|
18
|
+
const SOURCE_FILE = /\.(?:[cm]?[jt]sx?|vue|svelte|s?css|less)$/;
|
|
19
|
+
/** Reads a file; returns undefined (with a diagnostic) when unreadable or over the size limit. */
|
|
20
|
+
export async function readBounded(env, file) {
|
|
21
|
+
try {
|
|
22
|
+
const text = await env.fs.readFile(file);
|
|
23
|
+
if (text.length > env.limits.maxFileBytes) {
|
|
24
|
+
// Data files (web-types.json, source maps) are not read for components, so skipping them is not a finding.
|
|
25
|
+
if (!SOURCE_FILE.test(file))
|
|
26
|
+
return undefined;
|
|
27
|
+
// An installed package ships prebuilt bundles (`dist/vuetify.js`, aggregate `.d.ts`) far over the limit; the same
|
|
28
|
+
// components are in its smaller per-component files, so skipping them is information, not a warning.
|
|
29
|
+
const installed = [...(env.declarationRoots ?? [])].some((root) => file.startsWith(`${root}/`));
|
|
30
|
+
env.diagnostics[installed ? "info" : "warning"]("discovery.file-too-large", `Skipped ${file}: larger than ${env.limits.maxFileBytes} bytes`, {
|
|
31
|
+
location: { path: file }
|
|
32
|
+
});
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
env.diagnostics.warning("discovery.file-unreadable", `Could not read ${file}: ${error.message}`, { location: { path: file } });
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 1-based line of the first line at or after `fromLine` containing the quoted JSON key. */
|
|
43
|
+
export function lineOfJsonKey(text, key, fromLine = 1) {
|
|
44
|
+
const lines = text.split("\n");
|
|
45
|
+
const needle = `"${key}"`;
|
|
46
|
+
for (let i = fromLine - 1; i < lines.length; i++) {
|
|
47
|
+
if (lines[i].includes(needle))
|
|
48
|
+
return i + 1;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Line of `key` inside the top-level `group` object, e.g. a dependency inside "dependencies". */
|
|
53
|
+
export function lineOfJsonMember(text, group, key) {
|
|
54
|
+
const start = lineOfJsonKey(text, group);
|
|
55
|
+
return start === undefined ? undefined : lineOfJsonKey(text, key, start);
|
|
56
|
+
}
|
|
57
|
+
/** Directory of `file` is inside (or equal to) `dir`. */
|
|
58
|
+
export function isInside(dir, file) {
|
|
59
|
+
return dir === "." || file === dir || file.startsWith(`${dir}/`);
|
|
60
|
+
}
|
|
61
|
+
/** Owning package: the package whose directory is the longest prefix of `file`. */
|
|
62
|
+
export function owningPackage(packages, file) {
|
|
63
|
+
let best;
|
|
64
|
+
for (const pkg of packages) {
|
|
65
|
+
if (isInside(pkg.dir, file) && (!best || pkg.dir.length > best.dir.length))
|
|
66
|
+
best = pkg;
|
|
67
|
+
}
|
|
68
|
+
return best;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Case-exact existence. Walked files are authoritative (the real filesystem may be case-insensitive,
|
|
72
|
+
* so `exists("Button.ts")` can be true when only `button.ts` is present). Only paths inside ignored
|
|
73
|
+
* directories such as `dist/`, which the walk skips on purpose, fall back to the filesystem.
|
|
74
|
+
*/
|
|
75
|
+
export async function fileExists(env, file) {
|
|
76
|
+
if (env.fileSet.has(file))
|
|
77
|
+
return true;
|
|
78
|
+
if (!file.split("/").some((segment) => env.ignoreDirs.has(segment)))
|
|
79
|
+
return false;
|
|
80
|
+
return env.fs.exists(file);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=support.js.map
|