@reqvire-org/ads 0.0.0-stage → 0.2.9
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 +21 -0
- package/README.md +284 -2
- package/dist/assets.mjs +52 -0
- package/dist/bootstrap-ds/ADS.md +203 -0
- package/dist/bootstrap-ds/README.md +98 -0
- package/dist/bootstrap-ds/biome.css.json +21 -0
- package/dist/bootstrap-ds/design-system/assets/brand/README.md +28 -0
- package/dist/bootstrap-ds/design-system/assets/fonts/.gitkeep +0 -0
- package/dist/bootstrap-ds/design-system/components/.gitkeep +0 -0
- package/dist/bootstrap-ds/design-system/composition/.gitkeep +0 -0
- package/dist/bootstrap-ds/design-system/index.ts +5 -0
- package/dist/bootstrap-ds/design-system/patterns/.gitkeep +0 -0
- package/dist/bootstrap-ds/design-system/showcase/mcp/index.html +1 -0
- package/dist/bootstrap-ds/design-system/showcase/mcp/main.tsx +8 -0
- package/dist/bootstrap-ds/design-system/styles.css +5 -0
- package/dist/bootstrap-ds/design-system/tokens/Foundation.fixture.ts +47 -0
- package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +14 -0
- package/dist/bootstrap-ds/design-system/tokens/base.css +27 -0
- package/dist/bootstrap-ds/design-system/tokens/colors.css +29 -0
- package/dist/bootstrap-ds/design-system/tokens/elevation.css +15 -0
- package/dist/bootstrap-ds/design-system/tokens/spacing.css +15 -0
- package/dist/bootstrap-ds/design-system/tokens/typography.css +13 -0
- package/dist/bootstrap-ds/ds.config.json +13 -0
- package/dist/bootstrap-ds/gitignore.template +8 -0
- package/dist/bootstrap-ds/package-lock.json +4660 -0
- package/dist/bootstrap-ds/package.json +41 -0
- package/dist/bootstrap-ds/scripts/build-mcp-previews.mjs +33 -0
- package/dist/bootstrap-ds/src/vite-env.d.ts +7 -0
- package/dist/bootstrap-ds/tsconfig.build.json +12 -0
- package/dist/bootstrap-ds/tsconfig.json +20 -0
- package/dist/bootstrap-ds/vite.config.ts +21 -0
- package/dist/boundaries.mjs +130 -0
- package/dist/brand-assets.mjs +82 -0
- package/dist/check.mjs +222 -0
- package/dist/cli.mjs +106 -0
- package/dist/config.mjs +181 -0
- package/dist/contracts/tokens.json +52 -0
- package/dist/html.mjs +30 -0
- package/dist/index.mjs +9 -0
- package/dist/init.mjs +194 -0
- package/dist/lint-config.mjs +40 -0
- package/dist/mcp/THIRD_PARTY_NOTICES.md +786 -0
- package/dist/mcp/apps.mjs +53 -0
- package/dist/mcp/build-status.mjs +164 -0
- package/dist/mcp/catalog.mjs +65 -0
- package/dist/mcp/context.mjs +69 -0
- package/dist/mcp/mutations.mjs +301 -0
- package/dist/mcp/patch.mjs +114 -0
- package/dist/mcp/preview-watch.mjs +169 -0
- package/dist/mcp/preview-widget.bundle.js +113 -0
- package/dist/mcp/project-widget.bundle.js +113 -0
- package/dist/mcp/prompts.mjs +28 -0
- package/dist/mcp/resource-loader.js +138 -0
- package/dist/mcp/sandbox-host.mjs +22 -0
- package/dist/mcp/server.mjs +377 -0
- package/dist/mcp/showcase-host.mjs +68 -0
- package/dist/mcp/skills.mjs +77 -0
- package/dist/mcp/widget.bundle.js +150 -0
- package/dist/oxlint.mjs +81 -0
- package/dist/preview-resources.mjs +198 -0
- package/dist/project-documents.mjs +27 -0
- package/dist/public-props.mjs +74 -0
- package/dist/scenarios-compiler.mjs +287 -0
- package/dist/scenarios-vite.d.mts +3 -0
- package/dist/scenarios-vite.mjs +71 -0
- package/dist/scenarios.mjs +212 -0
- package/dist/showcase/client.bundle.js +127 -0
- package/dist/showcase/index.html +17 -0
- package/dist/showcase/sandbox.js +30 -0
- package/dist/showcase/style.css +7 -0
- package/dist/showcase-coverage.mjs +118 -0
- package/dist/showcase.mjs +77 -0
- package/dist/skills/README.md +68 -0
- package/dist/skills/ads-assimilate/SKILL.md +292 -0
- package/dist/skills/ads-assimilate/agents/openai.yaml +4 -0
- package/dist/skills/ads-assimilate/references/cli.md +111 -0
- package/dist/skills/ads-assimilate/references/lint-and-docs.md +87 -0
- package/dist/skills/ads-assimilate/references/native-implementation.md +98 -0
- package/dist/skills/ads-assimilate/references/testing.md +107 -0
- package/dist/skills/ads-break-ui/LICENSE.upstream +21 -0
- package/dist/skills/ads-break-ui/README.md +16 -0
- package/dist/skills/ads-break-ui/SKILL.md +235 -0
- package/dist/skills/ads-break-ui/agents/openai.yaml +4 -0
- package/dist/skills/ads-break-ui/references/catalog.md +135 -0
- package/dist/skills/ads-break-ui/references/verification.md +48 -0
- package/dist/skills/ads-designer/LICENSE.upstream +21 -0
- package/dist/skills/ads-designer/README.md +19 -0
- package/dist/skills/ads-designer/SKILL.md +400 -0
- package/dist/skills/ads-designer/agents/openai.yaml +4 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +203 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +98 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/biome.css.json +21 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/assets/brand/README.md +28 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/assets/fonts/.gitkeep +0 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/components/.gitkeep +0 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/composition/.gitkeep +0 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +5 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/.gitkeep +0 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/showcase/mcp/index.html +1 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/showcase/mcp/main.tsx +8 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/styles.css +5 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.fixture.ts +47 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +14 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/base.css +27 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/colors.css +29 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/elevation.css +15 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/spacing.css +15 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/typography.css +13 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +13 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/gitignore.template +8 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/package-lock.json +4660 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/package.json +41 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/scripts/build-mcp-previews.mjs +33 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/src/vite-env.d.ts +7 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +12 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +20 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +21 -0
- package/dist/skills/ads-designer/references/cli.md +111 -0
- package/dist/skills/ads-designer/references/design-craft.md +65 -0
- package/dist/skills/ads-designer/references/mobile.md +73 -0
- package/dist/skills/ads-import/SKILL.md +335 -0
- package/dist/skills/ads-import/agents/openai.yaml +4 -0
- package/dist/skills/ads-import/references/branding.md +44 -0
- package/dist/skills/ads-import/references/cli.md +111 -0
- package/dist/skills/ads-import/references/implementation.md +111 -0
- package/dist/skills/ads-import/references/tokens.md +96 -0
- package/dist/skills/ads-motion/LICENSE.upstream +21 -0
- package/dist/skills/ads-motion/README.md +22 -0
- package/dist/skills/ads-motion/SKILL.md +217 -0
- package/dist/skills/ads-motion/agents/openai.yaml +4 -0
- package/dist/skills/ads-motion/references/audit.md +44 -0
- package/dist/skills/ads-motion/references/build.md +81 -0
- package/dist/skills/ads-motion/references/performance.md +55 -0
- package/dist/skills/ads-motion/references/review.md +35 -0
- package/dist/skills/ads-motion/references/verification.md +39 -0
- package/dist/token-policy.mjs +10 -0
- package/dist/tokens.mjs +142 -0
- package/dist/ui-framework.mjs +59 -0
- package/dist/units.mjs +168 -0
- package/docs/ADS.md +310 -0
- package/docs/ARCHITECTURE.md +311 -0
- package/docs/CHANGELOG.md +106 -0
- package/docs/GUIDE.md +722 -0
- package/docs/MCP.md +690 -0
- package/package.json +107 -4
package/dist/tokens.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
import valueParser from "postcss-value-parser";
|
|
5
|
+
import { loadConfig, sourceFiles } from "./config.mjs";
|
|
6
|
+
import { foundationFiles, isSharedToken, tokenContract } from "./token-policy.mjs";
|
|
7
|
+
import { checkComponentVariable } from "./boundaries.mjs";
|
|
8
|
+
|
|
9
|
+
export { tokenContract };
|
|
10
|
+
|
|
11
|
+
export function references(value) {
|
|
12
|
+
const refs = [];
|
|
13
|
+
valueParser(value).walk((node) => {
|
|
14
|
+
if (node.type !== "function" || node.value !== "var") return;
|
|
15
|
+
const name = node.nodes.find((child) => child.type === "word")?.value;
|
|
16
|
+
if (name?.startsWith("--")) refs.push({ name, fallback: node.nodes.some((child) => child.type === "div" && child.value === ",") });
|
|
17
|
+
});
|
|
18
|
+
return refs;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function context(decl) {
|
|
22
|
+
const parts = [];
|
|
23
|
+
for (let parent = decl.parent; parent; parent = parent.parent) {
|
|
24
|
+
if (parent.type === "rule") parts.unshift(parent.selector);
|
|
25
|
+
// Layers affect precedence, not the theme condition.
|
|
26
|
+
if (parent.type === "atrule" && parent.name !== "layer") parts.unshift(`@${parent.name} ${parent.params}`);
|
|
27
|
+
}
|
|
28
|
+
return parts.join(" > ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function matchesType(value, type) {
|
|
32
|
+
if (/^(?:calc|min|max|clamp|env)\(/.test(value)) return true; // Browser expressions need runtime validation.
|
|
33
|
+
if (/^(?:inherit|initial|unset|revert(?:-layer)?)$/.test(value)) return true;
|
|
34
|
+
switch (type) {
|
|
35
|
+
case "color": return /^(?:#[\da-f]{3,8}|[a-z][\w-]*|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color|color-mix|light-dark)\([\s\S]+\))$/i.test(value);
|
|
36
|
+
case "dimension": return /^[-+]?(?:\d*\.)?\d+(?:px|r?em|ch|ex|cap|lh|rlh|v[wh]|[sld]v[wh]|vmin|vmax|cm|mm|in|pt|pc|%)$/i.test(value) || /^[-+]?0(?:\.0+)?$/.test(value);
|
|
37
|
+
case "duration": return /^[-+]?(?:\d*\.)?\d+m?s$/i.test(value);
|
|
38
|
+
case "number": return /^[-+]?(?:\d*\.)?\d+$/.test(value);
|
|
39
|
+
case "easing": return /^(?:linear|ease(?:-in|-out|-in-out)?|step-start|step-end|(?:cubic-bezier|steps|linear)\([\s\S]+\))$/.test(value);
|
|
40
|
+
case "shadow": return value === "none" || /(?:\d(?:px|rem|em)|var\()/.test(value);
|
|
41
|
+
case "fontFamily": return Boolean(value.trim()) && !/^[-+]?\d/.test(value);
|
|
42
|
+
default: return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function inspectTokens(directory) {
|
|
47
|
+
const options = loadConfig(directory);
|
|
48
|
+
const { root, dsRoot, config } = options;
|
|
49
|
+
const definitions = new Map();
|
|
50
|
+
const diagnostics = [];
|
|
51
|
+
const files = sourceFiles(path.join(dsRoot, config.tokenDir)).filter((file) => file.endsWith(".css"));
|
|
52
|
+
files.sort((a, b) => foundationFiles.indexOf(path.basename(a)) - foundationFiles.indexOf(path.basename(b)) || a.localeCompare(b));
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
let css;
|
|
55
|
+
try { css = postcss.parse(readFileSync(file, "utf8"), { from: file }); }
|
|
56
|
+
catch (error) {
|
|
57
|
+
diagnostics.push({ rule: "css-syntax", file: path.relative(root, file), line: error.line ?? 1, column: error.column ?? 1, message: error.reason ?? error.message });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
css.walkDecls((decl) => {
|
|
61
|
+
if (!decl.prop.startsWith("--")) return;
|
|
62
|
+
const ownership = checkComponentVariable(decl.prop, file, options);
|
|
63
|
+
if (ownership) diagnostics.push({ rule: ownership.rule, file: path.relative(root, file), line: decl.source.start.line, column: decl.source.start.column, message: ownership.message });
|
|
64
|
+
const definition = {
|
|
65
|
+
file: path.relative(root, file).replaceAll("\\", "/"),
|
|
66
|
+
line: decl.source.start.line,
|
|
67
|
+
column: decl.source.start.column,
|
|
68
|
+
scope: context(decl),
|
|
69
|
+
value: decl.value,
|
|
70
|
+
};
|
|
71
|
+
if (!definitions.has(decl.prop)) definitions.set(decl.prop, []);
|
|
72
|
+
definitions.get(decl.prop).push(definition);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const base = new Map();
|
|
76
|
+
const scopes = new Map();
|
|
77
|
+
for (const [name, entries] of definitions) {
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (entry.scope === ":root") base.set(name, entry);
|
|
80
|
+
else {
|
|
81
|
+
if (!scopes.has(entry.scope)) scopes.set(entry.scope, new Map());
|
|
82
|
+
scopes.get(entry.scope).set(name, entry);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const name of Object.keys(tokenContract.tokens)) {
|
|
87
|
+
if (!base.has(name)) diagnostics.push({ rule: "missing-token", file: config.dsDir, line: 1, column: 1, message: `Define ${name} in :root using the project's chosen value.` });
|
|
88
|
+
}
|
|
89
|
+
const emitted = new Set();
|
|
90
|
+
function report(rule, name, entry, message) {
|
|
91
|
+
const key = `${rule}:${name}:${entry.file}:${entry.line}:${message}`;
|
|
92
|
+
if (emitted.has(key)) return;
|
|
93
|
+
emitted.add(key);
|
|
94
|
+
diagnostics.push({ rule, ...entry, message });
|
|
95
|
+
}
|
|
96
|
+
const effectiveScopes = [base, ...[...scopes.values()].map((overrides) => new Map([...base, ...overrides]))];
|
|
97
|
+
for (const scope of effectiveScopes) {
|
|
98
|
+
const cache = new Map();
|
|
99
|
+
function resolveAlias(name, chain = []) {
|
|
100
|
+
if (cache.has(name)) return cache.get(name);
|
|
101
|
+
const entry = scope.get(name);
|
|
102
|
+
if (!entry) return undefined;
|
|
103
|
+
if (chain.includes(name)) {
|
|
104
|
+
report("token-cycle", name, entry, `Cyclic token alias: ${[...chain, name].join(" -> ")}`);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const resolvedReferences = new Map();
|
|
108
|
+
for (const ref of references(entry.value)) {
|
|
109
|
+
if (!isSharedToken(ref.name)) report("token-name", name, entry, `${name} references ${ref.name}, which does not follow ADS's fixed token naming contract.`);
|
|
110
|
+
if (!scope.has(ref.name) && !ref.fallback) report("token-reference", name, entry, `${name} references undefined ${ref.name}.`);
|
|
111
|
+
else if (scope.has(ref.name)) resolvedReferences.set(ref.name, resolveAlias(ref.name, [...chain, name]));
|
|
112
|
+
}
|
|
113
|
+
const parsed = valueParser(entry.value).nodes.filter((node) => node.type !== "space" && node.type !== "comment");
|
|
114
|
+
if (parsed.length === 1 && parsed[0].type === "function" && parsed[0].value === "var") {
|
|
115
|
+
const ref = references(entry.value)[0];
|
|
116
|
+
if (ref && scope.has(ref.name)) {
|
|
117
|
+
cache.set(name, resolvedReferences.get(ref.name));
|
|
118
|
+
return cache.get(name);
|
|
119
|
+
}
|
|
120
|
+
if (ref?.fallback) {
|
|
121
|
+
const comma = parsed[0].nodes.findIndex((node) => node.type === "div" && node.value === ",");
|
|
122
|
+
const fallback = valueParser.stringify(parsed[0].nodes.slice(comma + 1)).trim();
|
|
123
|
+
cache.set(name, fallback);
|
|
124
|
+
return fallback;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
cache.set(name, entry.value);
|
|
129
|
+
return entry.value;
|
|
130
|
+
}
|
|
131
|
+
for (const [name, entry] of scope) {
|
|
132
|
+
const resolved = resolveAlias(name);
|
|
133
|
+
const type = tokenContract.tokens[name]?.type;
|
|
134
|
+
if (type && resolved !== undefined && !matchesType(resolved, type)) report("token-type", name, entry, `${name} expects ${type}; found ${resolved}.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
contractVersion: tokenContract.version,
|
|
139
|
+
tokens: [...definitions].sort(([a], [b]) => a.localeCompare(b)).map(([name, entries]) => ({ name, ...tokenContract.tokens[name], definitions: entries })),
|
|
140
|
+
diagnostics,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
|
|
5
|
+
const modules = new Set(["react", "preact", "preact/compat", "preact/jsx-runtime"]);
|
|
6
|
+
|
|
7
|
+
/** Init checks explicit JSX choices without replacing the project's compiler. */
|
|
8
|
+
export function assertFrameworkCompiler(root, framework) {
|
|
9
|
+
const visited = new Set();
|
|
10
|
+
function inspect(file) {
|
|
11
|
+
file = path.resolve(file);
|
|
12
|
+
if (visited.has(file)) return;
|
|
13
|
+
visited.add(file);
|
|
14
|
+
const parsed = ts.readConfigFile(file, ts.sys.readFile);
|
|
15
|
+
if (parsed.error) throw new Error(`Cannot read ${path.relative(root, file)}: ${ts.flattenDiagnosticMessageText(parsed.error.messageText, "\n")}`);
|
|
16
|
+
const config = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(file));
|
|
17
|
+
const { jsx, jsxImportSource } = config.options;
|
|
18
|
+
const runtime = jsxImportSource ?? ([ts.JsxEmit.ReactJSX, ts.JsxEmit.ReactJSXDev].includes(jsx) ? "react" : undefined);
|
|
19
|
+
if (["react", "preact"].includes(runtime) && runtime !== framework) {
|
|
20
|
+
throw new Error(`${path.relative(root, file)} uses ${runtime} JSX, but the selected ADS framework is ${framework}. Align the project's JSX/compiler setup before running init; ADS does not migrate frameworks.`);
|
|
21
|
+
}
|
|
22
|
+
for (const reference of config.projectReferences ?? []) inspect(ts.resolveProjectReferencePath(reference));
|
|
23
|
+
}
|
|
24
|
+
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
25
|
+
const file = path.join(root, name);
|
|
26
|
+
if (existsSync(file)) inspect(file);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Resolve imported UI APIs by symbol identity, including aliased namespaces.
|
|
31
|
+
* A local function named h/createElement or a shadowed import is not a UI API. */
|
|
32
|
+
export function importedUiApi(expression, checker) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
let base = expression;
|
|
35
|
+
while (ts.isQualifiedName(base) || ts.isPropertyAccessExpression(base)) {
|
|
36
|
+
parts.unshift((base.right ?? base.name).text);
|
|
37
|
+
base = base.left ?? base.expression;
|
|
38
|
+
}
|
|
39
|
+
if (!ts.isIdentifier(base)) return null;
|
|
40
|
+
for (const declaration of checker.getSymbolAtLocation(base)?.declarations ?? []) {
|
|
41
|
+
if (ts.isImportSpecifier(declaration) && modules.has(declaration.parent.parent.parent.moduleSpecifier.text)) {
|
|
42
|
+
return { name: [(declaration.propertyName ?? declaration.name).text, ...parts].join("."), module: declaration.parent.parent.parent.moduleSpecifier.text };
|
|
43
|
+
}
|
|
44
|
+
if (parts.length && (ts.isNamespaceImport(declaration) || ts.isImportClause(declaration))) {
|
|
45
|
+
const imported = ts.isNamespaceImport(declaration) ? declaration.parent.parent : declaration.parent;
|
|
46
|
+
if (modules.has(imported.moduleSpecifier.text)) return { name: parts.join("."), module: imported.moduleSpecifier.text };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function importedUiName(expression, checker) {
|
|
53
|
+
return importedUiApi(expression, checker)?.name ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function uiOperation(expression, checker) {
|
|
57
|
+
const name = importedUiName(expression, checker);
|
|
58
|
+
return name === "h" ? "createElement" : name;
|
|
59
|
+
}
|
package/dist/units.mjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { isInside } from "./config.mjs";
|
|
5
|
+
import { importResolver } from "./boundaries.mjs";
|
|
6
|
+
import { isProjectDocument, projectGuideFiles } from "./project-documents.mjs";
|
|
7
|
+
|
|
8
|
+
const source = /\.[cm]?[jt]sx?$/;
|
|
9
|
+
const slash = file => file.replaceAll("\\", "/");
|
|
10
|
+
function fail(message, file) { return Object.assign(new Error(message), { diagnostic: { rule: "mutation-unit", message, ...(file ? { file } : {}) } }); }
|
|
11
|
+
|
|
12
|
+
/** Project setup and explicitly registered ADS extension/build entrypoints. */
|
|
13
|
+
export function configurationFiles(options) {
|
|
14
|
+
const { root, config, sourceRoot, dsRoot } = options;
|
|
15
|
+
const files = new Set(["ds.config.json", "package.json"]);
|
|
16
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
17
|
+
if (entry.isFile() && /^(?:(?:ts|js)config[^/]*\.jsonc?|(?:vite|vitest|linaria|babel|postcss|biome|oxlint)[^/]*\.(?:jsonc?|[cm]?[jt]s)|\.(?:oxlintrc|linariarc|babelrc)(?:\.json)?)$/.test(entry.name)) files.add(entry.name);
|
|
18
|
+
}
|
|
19
|
+
const resolve = importResolver(options), visited = new Set();
|
|
20
|
+
function visit(file, entrypoint = false, typeConfig = false) {
|
|
21
|
+
if (!isInside(root, file) || file === root || /(?:^|[\\/])(?:node_modules|\.git|\.ds|dist)(?:[\\/]|$)/.test(path.relative(root, file)) || visited.has(file) || !existsSync(file)) return;
|
|
22
|
+
if (!entrypoint && isInside(sourceRoot, file) && !isInside(dsRoot, file)) return;
|
|
23
|
+
if (!entrypoint && isInside(dsRoot, file) && /^(?:components|patterns|composition|tokens)[\\/]/.test(path.relative(dsRoot, file))) {
|
|
24
|
+
if (/\.[jt]sx$|\.(?:scenario|fixture)\./.test(file)) return;
|
|
25
|
+
if (source.test(file) && /(?:from\s*["'](?:react|preact|@linaria\/)|\b(?:require|import)\s*\(\s*["'](?:react|preact|@linaria\/))/.test(readFileSync(file,"utf8"))) return;
|
|
26
|
+
}
|
|
27
|
+
visited.add(file);
|
|
28
|
+
files.add(slash(path.relative(root, file)));
|
|
29
|
+
if (typeConfig) {
|
|
30
|
+
const parsed = ts.readConfigFile(file,ts.sys.readFile).config;
|
|
31
|
+
const relatives = [...(Array.isArray(parsed?.extends) ? parsed.extends : typeof parsed?.extends === "string" ? [parsed.extends] : []), ...(parsed?.references ?? []).map(item=>item.path)];
|
|
32
|
+
for (const relative of relatives) if (typeof relative === "string" && relative.startsWith(".")) {
|
|
33
|
+
const base=path.resolve(path.dirname(file),relative);
|
|
34
|
+
for (const target of [base,base+".json",path.join(base,"tsconfig.json")]) if (existsSync(target) && /\.jsonc?$/.test(target)) { visit(target,true,true); break; }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!source.test(file)) return;
|
|
38
|
+
const ast = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
|
|
39
|
+
function walk(node) {
|
|
40
|
+
const call = node.parent && ts.isCallExpression(node.parent) ? node.parent.expression : null;
|
|
41
|
+
const moduleCall = call && (call.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(call) && call.text === "require" || ts.isPropertyAccessExpression(call) && call.getText(ast) === "require.resolve");
|
|
42
|
+
if (ts.isStringLiteralLike(node) && (ts.isImportDeclaration(node.parent) || ts.isExportDeclaration(node.parent) || moduleCall)) {
|
|
43
|
+
for (const target of resolve(node.text, file)) if (source.test(target) || target.endsWith(".json")) visit(target);
|
|
44
|
+
}
|
|
45
|
+
ts.forEachChild(node, walk);
|
|
46
|
+
}
|
|
47
|
+
walk(ast);
|
|
48
|
+
}
|
|
49
|
+
for (const file of files) visit(path.resolve(root,file),true, /^(?:ts|js)config[^/]*\.jsonc?$/.test(file));
|
|
50
|
+
for (const plugin of config.oxlint.plugins) visit(path.resolve(root, plugin.path), true);
|
|
51
|
+
const pkg = existsSync(path.join(root, "package.json")) ? JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) : {};
|
|
52
|
+
const scripts = new Set();
|
|
53
|
+
function script(name) {
|
|
54
|
+
if (scripts.has(name)) return;
|
|
55
|
+
scripts.add(name);
|
|
56
|
+
const command = pkg.scripts?.[name] ?? "";
|
|
57
|
+
for (const match of command.matchAll(/\b(?:npm|bun)\s+run\s+([\w:.-]+)/g)) script(match[1]);
|
|
58
|
+
for (const match of command.matchAll(/\b(?:node|bun|tsx|ts-node)\s+([^\s;&|]+\.[cm]?[jt]sx?)(?=\s|$)/g)) visit(path.resolve(root, match[1]), true);
|
|
59
|
+
}
|
|
60
|
+
for (const name of ["lint:ds", "lint:style", "lint:project", "typecheck", "build:mcp-previews", "generate:icons", ...(config.mcp.mutationChecks ?? [])]) script(name);
|
|
61
|
+
const watch = config.mcp.previewWatch;
|
|
62
|
+
if (watch) {
|
|
63
|
+
if (["npm", "bun"].includes(watch.command) && watch.args[0] === "run") script(watch.args[1]);
|
|
64
|
+
else for (const argument of [watch.command, ...watch.args]) if (/\.[cm]?[jt]sx?$/.test(argument)) visit(path.resolve(root, argument), true);
|
|
65
|
+
}
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Ownership is derived from the existing flat/directory colocation convention. */
|
|
70
|
+
export function inspectUnits(options) {
|
|
71
|
+
const { root, dsRoot, config } = options;
|
|
72
|
+
const setup = configurationFiles(options);
|
|
73
|
+
const files = [];
|
|
74
|
+
function walk(directory) {
|
|
75
|
+
if (!existsSync(directory)) return;
|
|
76
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
77
|
+
if (entry.name.startsWith(".") || ["node_modules", "dist", "coverage"].includes(entry.name)) continue;
|
|
78
|
+
const file = path.join(directory, entry.name);
|
|
79
|
+
if (entry.isDirectory()) walk(file);
|
|
80
|
+
else if (entry.isFile()) files.push(slash(path.relative(dsRoot, file)));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
walk(dsRoot);
|
|
84
|
+
files.sort();
|
|
85
|
+
const directoryUnits = files.filter(file => /(?:^|\/)Scenario\.[cm]?[jt]sx?$/.test(file)).map(file => slash(path.dirname(file))).sort((a, b) => b.length - a.length);
|
|
86
|
+
const flatUnits = [...new Set(files.filter(file => /^(?:components|patterns|composition)\//.test(file) && source.test(file) && !directoryUnits.some(directory => file.startsWith(directory + "/"))).map(file => {
|
|
87
|
+
const base = path.basename(file).split(".")[0];
|
|
88
|
+
return slash(path.join(path.dirname(file), base));
|
|
89
|
+
}))].sort((a, b) => b.length - a.length);
|
|
90
|
+
const publicEntry = slash(path.relative(dsRoot, path.join(dsRoot, config.publicEntry)));
|
|
91
|
+
const tokenDir = slash(path.relative(dsRoot, path.join(dsRoot, config.tokenDir)));
|
|
92
|
+
function owner(relative) {
|
|
93
|
+
const projectFile = slash(path.relative(root, path.resolve(dsRoot, relative)));
|
|
94
|
+
if (setup.has(projectFile)) return "configuration";
|
|
95
|
+
if (isProjectDocument(projectFile)) return "documentation";
|
|
96
|
+
if (!isInside(dsRoot, path.resolve(dsRoot, relative))) return null;
|
|
97
|
+
if (relative === publicEntry) return null; // Controlled re-export bookkeeping.
|
|
98
|
+
if (relative.startsWith(tokenDir + "/")) return "tokens";
|
|
99
|
+
if (relative.startsWith("assets/")) return relative.split("/").length > 2 ? relative.split("/").slice(0, 2).join("/") : "assets";
|
|
100
|
+
if (!relative.includes("/")) return /(?:\.(?:md|txt)$|^LICENSE$)/i.test(relative) ? "documentation" : "foundation";
|
|
101
|
+
if (!/^(?:components|patterns|composition)\//.test(relative)) return relative.split("/")[0];
|
|
102
|
+
for (const directory of directoryUnits) if (relative.startsWith(directory + "/")) return directory;
|
|
103
|
+
for (const unit of flatUnits) if (relative.startsWith(unit + "/") || relative.startsWith(unit + ".")) return unit;
|
|
104
|
+
const base = path.basename(relative).split(".")[0];
|
|
105
|
+
return slash(path.join(path.dirname(relative), base));
|
|
106
|
+
}
|
|
107
|
+
const units = new Map();
|
|
108
|
+
units.set("configuration", { id: "configuration", kind: "configuration", files: [...setup].filter(file => existsSync(path.join(root, file))).sort() });
|
|
109
|
+
for (const id of ["foundation", "documentation", "tokens", "assets/brand", "assets/fonts"]) units.set(id, { id, kind: id.split("/")[0], files: [] });
|
|
110
|
+
units.get("documentation").files = projectGuideFiles(root);
|
|
111
|
+
for (const relative of files) {
|
|
112
|
+
const id = owner(relative);
|
|
113
|
+
if (!id) continue;
|
|
114
|
+
if (!units.has(id)) units.set(id, { id, kind: id === "tokens" ? "tokens" : id.split("/")[0], files: [] });
|
|
115
|
+
const file = slash(path.relative(root, path.join(dsRoot, relative)));
|
|
116
|
+
if (!units.get(id).files.includes(file)) units.get(id).files.push(file);
|
|
117
|
+
}
|
|
118
|
+
return { units: [...units.values()].sort((a, b) => a.id.localeCompare(b.id)), owner, publicEntry,
|
|
119
|
+
relative: file => slash(path.relative(dsRoot, path.resolve(root, file))) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validateUnitId(unit) {
|
|
123
|
+
if (typeof unit !== "string" || unit.length > 256 || !/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(unit) || ["components", "patterns", "composition"].includes(unit)) throw fail("Select one ADS unit ID from ads_units or an unoccupied owner path following its colocation convention.");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Reject arbitrary file batches, even when every path is inside the ADS. */
|
|
127
|
+
export function checkUnitFiles(options, unit, changes, inventory = inspectUnits(options), allowNew = false) {
|
|
128
|
+
validateUnitId(unit);
|
|
129
|
+
for (const change of changes) {
|
|
130
|
+
const relative = inventory.relative(change.path);
|
|
131
|
+
if (allowNew && unit === "configuration") continue; // The candidate must register every setup/extension file.
|
|
132
|
+
if (relative === inventory.publicEntry) continue;
|
|
133
|
+
if (allowNew && change.originalContent === null && (relative.startsWith(unit + "/") || relative.startsWith(unit + "."))) continue;
|
|
134
|
+
const owner = inventory.owner(relative);
|
|
135
|
+
if (owner !== unit) throw fail(`This patch targets ${unit}; ${change.path} belongs to ${owner ?? "a different ADS layer"}. Submit a separate unit patch.`, change.path);
|
|
136
|
+
}
|
|
137
|
+
return inventory;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A candidate may establish a new directory unit, but cannot capture its peers. */
|
|
141
|
+
export function checkUnitCandidate(afterOptions, unit, changes, before) {
|
|
142
|
+
const after = inspectUnits(afterOptions);
|
|
143
|
+
checkUnitFiles(afterOptions, unit, changes.filter(change => change.content !== null || before.owner(before.relative(change.path)) !== unit), after);
|
|
144
|
+
for (const entry of before.units) for (const file of entry.files) {
|
|
145
|
+
if (!existsSync(path.join(afterOptions.root, file))) continue;
|
|
146
|
+
const relative = after.relative(file);
|
|
147
|
+
if (unit === "configuration" && (entry.id === "configuration" || after.owner(relative) === "configuration")) continue;
|
|
148
|
+
if (after.owner(relative) !== entry.id) throw fail("A patch cannot change ownership of existing peer units. Choose an unoccupied owner path.", file);
|
|
149
|
+
}
|
|
150
|
+
const exportChange = changes.find(change => before.relative(change.path) === before.publicEntry);
|
|
151
|
+
if (exportChange) {
|
|
152
|
+
if (!["components", "patterns"].includes(unit.split("/")[0])) throw fail("Only a component/pattern unit may update its public re-exports.", exportChange.path);
|
|
153
|
+
const current = exportChange.originalContent ?? "", proposed = exportChange.content;
|
|
154
|
+
if (proposed === null) throw fail("The shared public entry cannot be deleted.", exportChange.path);
|
|
155
|
+
const resolve = importResolver(afterOptions);
|
|
156
|
+
function unrelated(content) {
|
|
157
|
+
const file = path.join(afterOptions.dsRoot, afterOptions.config.publicEntry);
|
|
158
|
+
const ast = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
|
|
159
|
+
return ast.statements.filter(statement => {
|
|
160
|
+
if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) return true;
|
|
161
|
+
const targets = resolve(statement.moduleSpecifier.text, file);
|
|
162
|
+
return !targets.length || targets.some(target => !isInside(afterOptions.dsRoot, target) || after.owner(slash(path.relative(afterOptions.dsRoot, target))) !== unit);
|
|
163
|
+
}).map(statement => statement.getText(ast)).join("\n");
|
|
164
|
+
}
|
|
165
|
+
if (unrelated(current) !== unrelated(proposed)) throw fail("Public-entry patches may change only re-exports from the selected unit; preserve all other statements.", exportChange.path);
|
|
166
|
+
}
|
|
167
|
+
if (!before.units.some(entry => entry.id === unit) && !after.units.some(entry => entry.id === unit)) throw fail("The patched files do not establish the selected ADS unit.");
|
|
168
|
+
}
|