@reqvire-org/ads 0.0.0-stage → 0.3.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 +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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "native-design-system",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Private starter workspace for a project's local React design system.",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.12.0"
|
|
9
|
+
},
|
|
10
|
+
"sideEffects": [
|
|
11
|
+
"**/*.css"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "ads showcase",
|
|
15
|
+
"dev:showcase": "ads showcase",
|
|
16
|
+
"lint": "oxlint --deny-warnings src design-system && biome lint --config-path=biome.css.json .",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"build": "npm run lint && npm run typecheck && npm run build:lib",
|
|
19
|
+
"build:lib": "vite build && tsc --project tsconfig.build.json",
|
|
20
|
+
"build:showcase": "node scripts/build-mcp-previews.mjs",
|
|
21
|
+
"preview": "ads showcase",
|
|
22
|
+
"preview:showcase": "ads showcase",
|
|
23
|
+
"build:mcp-previews": "node scripts/build-mcp-previews.mjs"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "2.5.15",
|
|
27
|
+
"@babel/preset-react": "7.29.7",
|
|
28
|
+
"@babel/preset-typescript": "7.29.7",
|
|
29
|
+
"@linaria/atomic": "8.0.0",
|
|
30
|
+
"@types/node": "25.9.4",
|
|
31
|
+
"@types/react": "19.2.17",
|
|
32
|
+
"@types/react-dom": "19.2.3",
|
|
33
|
+
"@vitejs/plugin-react": "4.7.0",
|
|
34
|
+
"@wyw-in-js/vite": "2.1.3",
|
|
35
|
+
"oxlint": "1.72.0",
|
|
36
|
+
"react": "19.2.7",
|
|
37
|
+
"react-dom": "19.2.7",
|
|
38
|
+
"typescript": "5.9.3",
|
|
39
|
+
"vite": "6.4.3"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { build } from "vite";
|
|
6
|
+
import wyw from "@wyw-in-js/vite";
|
|
7
|
+
import react from "@vitejs/plugin-react";
|
|
8
|
+
import { runScenarioCompiler } from "@reqvire-org/ads/scenarios/compiler";
|
|
9
|
+
import { createPreviewResources, previewEntriesPlugin } from "@reqvire-org/ads/scenarios/vite";
|
|
10
|
+
|
|
11
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
12
|
+
const runScript = promisify(execFile);
|
|
13
|
+
const projectPackage = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
14
|
+
const manager = projectPackage.packageManager?.split("@")[0] ?? (existsSync(path.join(root, "bun.lock")) || existsSync(path.join(root, "bun.lockb")) ? "bun" : "npm");
|
|
15
|
+
await runScenarioCompiler(root, {
|
|
16
|
+
watched: process.argv.includes("--watch"),
|
|
17
|
+
compilerFiles: [import.meta.filename],
|
|
18
|
+
async validate() {
|
|
19
|
+
try { await runScript(manager, ["run", "lint"], { cwd: root }); }
|
|
20
|
+
catch (error) { throw new Error([error.stdout, error.stderr].map(value => value?.toString() ?? "").join("\n") || error.message); }
|
|
21
|
+
},
|
|
22
|
+
async buildGraph(entries, options, track) {
|
|
23
|
+
const mount = path.join(options.dsRoot, "showcase/mcp/main.tsx");
|
|
24
|
+
const output = await build({ configFile: false, root, base: "./", publicDir: false,
|
|
25
|
+
plugins: [previewEntriesPlugin(entries.map(entry => ({ ...entry, root })), mount), wyw({ include: ["**/*.{ts,tsx}"], exclude: ["**/node_modules/**"] }), react(), track],
|
|
26
|
+
resolve: { alias: { "@ds": path.join(options.dsRoot, options.config.publicEntry) } },
|
|
27
|
+
worker: { format: "iife" },
|
|
28
|
+
build: { write: false, cssCodeSplit: true, assetsInlineLimit: 0, modulePreload: false,
|
|
29
|
+
rollupOptions: { preserveEntrySignatures: "strict", input: Object.fromEntries(entries.map((_entry, index) => [`scenario-${index}`, `ads-preview-entry:${index}`])) } },
|
|
30
|
+
});
|
|
31
|
+
return createPreviewResources(output, options, entries);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"emitDeclarationOnly": true,
|
|
7
|
+
"rootDir": "design-system",
|
|
8
|
+
"outDir": "dist"
|
|
9
|
+
},
|
|
10
|
+
"include": ["design-system/**/*.ts", "design-system/**/*.tsx"],
|
|
11
|
+
"exclude": ["design-system/showcase"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"isolatedModules": true,
|
|
9
|
+
"moduleDetection": "force",
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"noUnusedParameters": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"baseUrl": ".",
|
|
17
|
+
"paths": { "@ds": ["design-system/index.ts"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["src", "design-system", "vite.config.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
import wyw from "@wyw-in-js/vite";
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [wyw(), react()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: { "@ds": path.resolve(import.meta.dirname, "design-system/index.ts") },
|
|
10
|
+
},
|
|
11
|
+
build: {
|
|
12
|
+
outDir: "dist",
|
|
13
|
+
lib: {
|
|
14
|
+
entry: path.resolve(import.meta.dirname, "design-system/index.ts"),
|
|
15
|
+
formats: ["es"],
|
|
16
|
+
fileName: () => "index.js",
|
|
17
|
+
cssFileName: "styles",
|
|
18
|
+
},
|
|
19
|
+
rollupOptions: { external: [/^react(?:\/.*)?$/, /^react-dom(?:\/.*)?$/] },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { importedUiName } from "./ui-framework.mjs";
|
|
4
|
+
import { isInside, isStateVariable, isTestFile, isDevelopmentFile } from "./config.mjs";
|
|
5
|
+
import { isSharedToken } from "./token-policy.mjs";
|
|
6
|
+
|
|
7
|
+
export function projectCompilerOptions({ root }) {
|
|
8
|
+
const file = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
|
|
9
|
+
let compiler = {};
|
|
10
|
+
if (file && isInside(root, file)) {
|
|
11
|
+
const parsed = ts.readConfigFile(file, ts.sys.readFile);
|
|
12
|
+
if (!parsed.error) compiler = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(file)).options;
|
|
13
|
+
}
|
|
14
|
+
return compiler;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function importResolver(options) {
|
|
18
|
+
const { root } = options;
|
|
19
|
+
const compiler = projectCompilerOptions(options);
|
|
20
|
+
return (specifier, filename) => {
|
|
21
|
+
// Vite and other bundlers attach resource queries to the same source file.
|
|
22
|
+
// They cannot turn a private file into a public import.
|
|
23
|
+
specifier = specifier.replace(/[?#].*$/, "");
|
|
24
|
+
const resolved = ts.resolveModuleName(specifier, filename, compiler, ts.sys).resolvedModule?.resolvedFileName;
|
|
25
|
+
if (resolved) return [path.resolve(resolved)];
|
|
26
|
+
// Unresolved local imports are still boundaries, even before their files exist.
|
|
27
|
+
if (specifier.startsWith(".")) return [path.resolve(path.dirname(filename), specifier)];
|
|
28
|
+
for (const [alias, targets] of Object.entries(compiler.paths ?? {})) {
|
|
29
|
+
const index = alias.indexOf("*");
|
|
30
|
+
const prefix = index < 0 ? alias : alias.slice(0, index);
|
|
31
|
+
const suffix = index < 0 ? "" : alias.slice(index + 1);
|
|
32
|
+
if (index < 0 ? specifier !== alias : !specifier.startsWith(prefix) || !specifier.endsWith(suffix)) continue;
|
|
33
|
+
const matched = index < 0 ? "" : specifier.slice(prefix.length, specifier.length - suffix.length);
|
|
34
|
+
return targets.map((target) => path.resolve(compiler.baseUrl ?? compiler.pathsBasePath ?? root, target.replace("*", matched)));
|
|
35
|
+
}
|
|
36
|
+
return [path.resolve(compiler.baseUrl ?? root, specifier)];
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function appImportViolation(specifier, filename, options, resolve = importResolver(options)) {
|
|
41
|
+
const { root, dsRoot, config } = options;
|
|
42
|
+
if (!isInside(dsRoot, filename) || isTestFile(filename)) return false;
|
|
43
|
+
const sourceRoot = path.resolve(root, config.sourceDir);
|
|
44
|
+
return resolve(specifier, filename).some((file) => isInside(sourceRoot, file) && !isInside(dsRoot, file));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function showcaseImportViolation(specifier, filename, options, resolve = importResolver(options)) {
|
|
48
|
+
if (isTestFile(filename)) return false;
|
|
49
|
+
if (isDevelopmentFile(filename, options)) return false;
|
|
50
|
+
return resolve(specifier, filename).some((file) => isDevelopmentFile(file, options) || isTestFile(file));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
export function checkComponentVariable(name, filename, options) {
|
|
55
|
+
const { dsRoot, config } = options;
|
|
56
|
+
if (!name.startsWith("--")) return null;
|
|
57
|
+
const tokens = isInside(path.join(dsRoot, config.tokenDir), filename);
|
|
58
|
+
const development = isDevelopmentFile(filename, options);
|
|
59
|
+
const components = !development && isInside(path.join(dsRoot, "components"), filename);
|
|
60
|
+
const patterns = !development && isInside(path.join(dsRoot, "patterns"), filename);
|
|
61
|
+
const showcase = development;
|
|
62
|
+
if (isSharedToken(name)) return tokens ? null : { rule: "token-ownership", message: `Define ${name} in ADS tokens, not component, pattern, showcase, or app styles.` };
|
|
63
|
+
if (name.startsWith("--ux-")) return patterns || showcase ? null : { rule: "variable-ownership", message: "--ux-* variables belong to ADS patterns and showcase composition." };
|
|
64
|
+
if (name.startsWith("--showcase-")) return showcase ? null : { rule: "variable-ownership", message: "--showcase-* variables belong to the ADS showcase." };
|
|
65
|
+
if (!name.startsWith("--ds-")) return { rule: "token-name", message: `${name} does not follow ADS's fixed token naming contract. Use a shared token, --ds-*, --ux-*, or --showcase-* in its owning layer.` };
|
|
66
|
+
if (components) return null;
|
|
67
|
+
if (isStateVariable(name)) return { rule: "primitive-state", message: "Primitive interaction variables belong to components. Add a typed primitive variant instead of overriding state policy." };
|
|
68
|
+
if (tokens) return { rule: "variable-ownership", message: "Component --ds-* variables belong to components, not shared token files." };
|
|
69
|
+
if (!config.boundaries.componentVariables.includes(name)) return { rule: "component-variable", message: `Undocumented component customization ${name}. Register a documented non-state variable or use a public prop.` };
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function memberName(node) {
|
|
74
|
+
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
|
75
|
+
if (ts.isElementAccessExpression(node) && node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isMember(node) { return ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node); }
|
|
80
|
+
function unwrapped(node) {
|
|
81
|
+
while (node?.parent && (ts.isParenthesizedExpression(node.parent) || ts.isAsExpression(node.parent) || ts.isNonNullExpression(node.parent) || ts.isSatisfiesExpression(node.parent))) node = node.parent;
|
|
82
|
+
return node;
|
|
83
|
+
}
|
|
84
|
+
function cssName(name) { return name?.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); }
|
|
85
|
+
function isWrite(node) {
|
|
86
|
+
node = unwrapped(node);
|
|
87
|
+
const parent = node.parent;
|
|
88
|
+
return (ts.isBinaryExpression(parent) && parent.left === node && parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
|
|
89
|
+
|| ((ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(parent.operator))
|
|
90
|
+
|| ts.isDeleteExpression(parent);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Inspects style access itself, so aliases, computed keys, cssText and bulk writes
|
|
94
|
+
// cannot silently turn a narrow property exception into a whole-file exception.
|
|
95
|
+
export function styleWriteViolation(node, filename, { root, config }) {
|
|
96
|
+
const allowed = config.boundaries.styleWrites.find((boundary) => path.resolve(root, boundary.file) === filename)?.properties ?? [];
|
|
97
|
+
if (isMember(node) && memberName(node) === "style") {
|
|
98
|
+
const access = unwrapped(node);
|
|
99
|
+
const parent = access.parent;
|
|
100
|
+
let property;
|
|
101
|
+
if (isMember(parent) && parent.expression === access) {
|
|
102
|
+
property = cssName(memberName(parent));
|
|
103
|
+
const target = unwrapped(parent);
|
|
104
|
+
if (ts.isCallExpression(target.parent) && target.parent.expression === target) {
|
|
105
|
+
if (["set-property", "remove-property"].includes(property)) {
|
|
106
|
+
const name = target.parent.arguments[0];
|
|
107
|
+
property = name && ts.isStringLiteralLike(name) ? name.text : null;
|
|
108
|
+
} else if (["get-property-value", "get-property-priority", "item"].includes(property)) return null;
|
|
109
|
+
else property = null;
|
|
110
|
+
} else if (!isWrite(target)) {
|
|
111
|
+
// Plain property reads are allowed. Passing a method reference or a
|
|
112
|
+
// computed member elsewhere is not a known property read.
|
|
113
|
+
if (property && !["set-property", "remove-property"].includes(property)) return null;
|
|
114
|
+
property = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (property && property !== "css-text" && allowed.includes(property)) return null;
|
|
118
|
+
return "Imperative styles require a file boundary and an explicit property. Bulk writes, computed property names, and style-object escapes are forbidden.";
|
|
119
|
+
}
|
|
120
|
+
if (ts.isCallExpression(node) && isMember(node.expression) && ["setAttribute", "setAttributeNS"].includes(memberName(node.expression))) {
|
|
121
|
+
const index = memberName(node.expression) === "setAttributeNS" ? 1 : 0;
|
|
122
|
+
const name = node.arguments[index];
|
|
123
|
+
if (name && ts.isStringLiteralLike(name) && name.text.toLowerCase() === "style") return "Do not write a style attribute. Use ADS props or a narrowly configured property boundary.";
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isUiCssProperties(node, checker) {
|
|
129
|
+
return ts.isTypeReferenceNode(node) && ["CSSProperties", "JSX.CSSProperties"].includes(importedUiName(node.typeName, checker));
|
|
130
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isInside, loadConfig } from "./config.mjs";
|
|
4
|
+
|
|
5
|
+
export const brandFiles = ["favicon.svg", "favicon.ico", "apple-touch-icon.png", "icon-192.png", "icon-512.png"];
|
|
6
|
+
|
|
7
|
+
export function brandSourceDiagnostic({ root, dsRoot, config }) {
|
|
8
|
+
if (!config.assets.iconSource) return null;
|
|
9
|
+
const file = path.join(dsRoot, config.assets.iconSource);
|
|
10
|
+
let valid = true;
|
|
11
|
+
for (let current = file; isInside(root, current); current = path.dirname(current)) {
|
|
12
|
+
const stat = lstatSync(current, { throwIfNoEntry: false });
|
|
13
|
+
if (!stat || stat.isSymbolicLink() || (current === file ? !stat.isFile() : !stat.isDirectory())) valid = false;
|
|
14
|
+
if (current === root) break;
|
|
15
|
+
}
|
|
16
|
+
if (valid && /<svg\b[\s\S]*<\/svg\s*>|<svg\b[^>]*\/>/i.test(readFileSync(file, "utf8"))) return null;
|
|
17
|
+
return { rule: "asset-source", file: path.relative(root, file).replaceAll("\\", "/"), line: 1, column: 1, message: "assets.iconSource must be an existing SVG master inside assets/brand/, without symlinked paths." };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertOutput(root, file, { directory = false } = {}) {
|
|
21
|
+
for (let current = file; isInside(root, current); current = path.dirname(current)) {
|
|
22
|
+
const stat = lstatSync(current, { throwIfNoEntry: false });
|
|
23
|
+
if (stat && (stat.isSymbolicLink() || (current === file && !directory ? !stat.isFile() : !stat.isDirectory()))) throw new Error(`Invalid generated asset path: ${current}`);
|
|
24
|
+
if (current === root) break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ico(entries) {
|
|
29
|
+
const offset = 6 + entries.length * 16;
|
|
30
|
+
const result = Buffer.alloc(offset + entries.reduce((size, entry) => size + entry.buffer.length, 0));
|
|
31
|
+
result.writeUInt16LE(1, 2);
|
|
32
|
+
result.writeUInt16LE(entries.length, 4);
|
|
33
|
+
let position = offset;
|
|
34
|
+
entries.forEach(({ size, buffer }, index) => {
|
|
35
|
+
const start = 6 + index * 16;
|
|
36
|
+
result.writeUInt8(size, start);
|
|
37
|
+
result.writeUInt8(size, start + 1);
|
|
38
|
+
result.writeUInt16LE(1, start + 4);
|
|
39
|
+
result.writeUInt16LE(32, start + 6);
|
|
40
|
+
result.writeUInt32LE(buffer.length, start + 8);
|
|
41
|
+
result.writeUInt32LE(position, start + 12);
|
|
42
|
+
buffer.copy(result, position);
|
|
43
|
+
position += buffer.length;
|
|
44
|
+
});
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function generateBrandAssets(directory = process.cwd()) {
|
|
49
|
+
const options = loadConfig(directory);
|
|
50
|
+
const { root, dsRoot, config } = options;
|
|
51
|
+
const output = path.join(root, ".ds/assets");
|
|
52
|
+
if (!config.assets.iconSource) return { directory: output, files: [], skipped: true };
|
|
53
|
+
const diagnostic = brandSourceDiagnostic(options);
|
|
54
|
+
if (diagnostic) throw new Error(`${diagnostic.file} [${diagnostic.rule}] ${diagnostic.message}`);
|
|
55
|
+
assertOutput(root, output, { directory: true });
|
|
56
|
+
for (const name of brandFiles) {
|
|
57
|
+
assertOutput(root, path.join(output, name));
|
|
58
|
+
for (const mount of [path.join(dsRoot, "assets"), path.join(dsRoot, "showcase/public/assets"), ...config.assets.publicDirs.map((entry) => path.join(root, entry))]) {
|
|
59
|
+
if (lstatSync(path.join(mount, name), { throwIfNoEntry: false })) throw new Error(`[asset-collision] ${path.relative(root, mount)}/${name} conflicts with a generated brand asset.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Load rendering only when the project has selected artwork. Render everything
|
|
63
|
+
// before writing, so invalid SVG cannot replace the last successful outputs.
|
|
64
|
+
const { default: sharp } = await import("sharp");
|
|
65
|
+
const source = readFileSync(path.join(dsRoot, config.assets.iconSource));
|
|
66
|
+
const outputs = new Map([["favicon.svg", source]]);
|
|
67
|
+
const sizes = [16, 32, 48, 180, 192, 512];
|
|
68
|
+
const rendered = new Map();
|
|
69
|
+
for (const size of sizes) rendered.set(size, await sharp(source, { density: 512 }).resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toBuffer());
|
|
70
|
+
outputs.set("favicon.ico", ico([16, 32, 48].map((size) => ({ size, buffer: rendered.get(size) }))));
|
|
71
|
+
outputs.set("apple-touch-icon.png", rendered.get(180));
|
|
72
|
+
outputs.set("icon-192.png", rendered.get(192));
|
|
73
|
+
outputs.set("icon-512.png", rendered.get(512));
|
|
74
|
+
mkdirSync(path.dirname(output), { recursive: true });
|
|
75
|
+
const staging = mkdtempSync(path.join(root, ".ds/assets-staging-"));
|
|
76
|
+
try {
|
|
77
|
+
for (const [name, buffer] of outputs) writeFileSync(path.join(staging, name), buffer);
|
|
78
|
+
mkdirSync(output, { recursive: true });
|
|
79
|
+
for (const name of brandFiles) renameSync(path.join(staging, name), path.join(output, name));
|
|
80
|
+
} finally { rmSync(staging, { recursive: true, force: true }); }
|
|
81
|
+
return { directory: output, files: brandFiles.map((name) => path.join(output, name)), skipped: false };
|
|
82
|
+
}
|
package/dist/check.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import postcss from "postcss";
|
|
5
|
+
import valueParser from "postcss-value-parser";
|
|
6
|
+
import { importViolation, isInside, loadConfig, sourceFiles, isDevelopmentFile, isTestFile } from "./config.mjs";
|
|
7
|
+
import { inspectTokens, references } from "./tokens.mjs";
|
|
8
|
+
import { foundationFiles, isSharedToken } from "./token-policy.mjs";
|
|
9
|
+
import { inspectAssets } from "./assets.mjs";
|
|
10
|
+
import { appImportViolation, showcaseImportViolation, checkComponentVariable, importResolver, projectCompilerOptions, isUiCssProperties, styleWriteViolation } from "./boundaries.mjs";
|
|
11
|
+
import { inspectPublicProps } from "./public-props.mjs";
|
|
12
|
+
import { inspectHtml, isMountCss } from "./html.mjs";
|
|
13
|
+
import { inspectShowcasePages } from "./showcase.mjs";
|
|
14
|
+
import { inspectShowcaseCoverage } from "./showcase-coverage.mjs";
|
|
15
|
+
import { inspectScenarioCoverage } from "./scenarios.mjs";
|
|
16
|
+
import { uiOperation, importedUiApi } from "./ui-framework.mjs";
|
|
17
|
+
|
|
18
|
+
const showcaseControls = new Set(["a", "button", "input", "select", "textarea", "option", "optgroup", "table", "thead", "tbody", "tfoot", "tr", "td", "th", "dialog", "details", "summary", "progress", "meter"]);
|
|
19
|
+
const svgTags = new Set(["svg", "g", "path", "circle", "rect", "ellipse", "line", "polyline", "polygon", "text", "tspan", "use", "defs", "clipPath", "mask", "filter", "linearGradient", "radialGradient", "stop", "marker", "pattern"]);
|
|
20
|
+
const svgPresentation = new Set(["strokeWidth", "strokeLinecap", "strokeLinejoin", "strokeDasharray", "strokeDashoffset", "strokeOpacity", "strokeMiterlimit", "fillRule", "fillOpacity", "clipRule", "clipPath", "colorInterpolation", "colorInterpolationFilters", "floodColor", "floodOpacity", "stopColor", "stopOpacity", "fontFamily", "fontSize", "fontStyle", "fontWeight", "letterSpacing", "textAnchor", "dominantBaseline", "markerStart", "markerMid", "markerEnd", "vectorEffect", "shapeRendering", "textRendering", "paintOrder", "alignmentBaseline", "baselineShift", "transformOrigin"]);
|
|
21
|
+
const widgetRoles = new Set(["button", "link", "checkbox", "radio", "switch", "combobox", "textbox", "listbox", "option", "slider", "spinbutton", "tab", "tablist", "menu", "menuitem", "tree", "treeitem", "grid", "dialog", "alertdialog", "progressbar"]);
|
|
22
|
+
|
|
23
|
+
export function checkProject(directory, { trackingRoot } = {}) {
|
|
24
|
+
const options = { ...loadConfig(directory), ...(trackingRoot ? { trackingRoot: path.resolve(trackingRoot) } : {}) };
|
|
25
|
+
const { root, config, dsRoot, sourceRoot } = options;
|
|
26
|
+
const diagnostics = [];
|
|
27
|
+
for (const [field, file] of [["dsDir", dsRoot], ["publicEntry", path.join(dsRoot, config.publicEntry)], ["stylesEntry", path.join(dsRoot, config.stylesEntry)]]) {
|
|
28
|
+
if (!existsSync(file) || (field === "dsDir" ? !statSync(file).isDirectory() : !statSync(file).isFile())) diagnostics.push({ rule: "project-structure", file: path.relative(root, file), line: 1, column: 1, message: `Missing or invalid ${field}. Bootstrap an empty ADS or configure its actual path.` });
|
|
29
|
+
}
|
|
30
|
+
if (diagnostics.length) return { ok: false, diagnostics };
|
|
31
|
+
const tokenInventory = inspectTokens(root);
|
|
32
|
+
const declaredTokens = new Set(tokenInventory.tokens.map((token) => token.name));
|
|
33
|
+
diagnostics.push(...tokenInventory.diagnostics);
|
|
34
|
+
diagnostics.push(...inspectAssets(options));
|
|
35
|
+
const resolveImport = importResolver(options);
|
|
36
|
+
const rootHtml = path.join(root, "index.html");
|
|
37
|
+
const files = [...new Set([...sourceFiles(sourceRoot), ...sourceFiles(dsRoot), ...(existsSync(rootHtml) ? [rootHtml] : [])])];
|
|
38
|
+
const scriptFiles = files.filter((file) => /\.[cm]?[jt]sx?$/.test(file));
|
|
39
|
+
const compiler = projectCompilerOptions(options);
|
|
40
|
+
const program = ts.createProgram(scriptFiles, { ...compiler, paths: { ...compiler.paths, [config.publicAlias]: [path.join(dsRoot, config.publicEntry)] }, allowJs: true, noResolve: true, noLib: true, types: [], target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, jsx: ts.JsxEmit.Preserve });
|
|
41
|
+
const checker = program.getTypeChecker();
|
|
42
|
+
function report(file, rule, message, line = 1, column = 1) {
|
|
43
|
+
diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column });
|
|
44
|
+
}
|
|
45
|
+
inspectShowcasePages(options, program, resolveImport, report);
|
|
46
|
+
inspectShowcaseCoverage(options, program, report);
|
|
47
|
+
inspectScenarioCoverage(options, program, report);
|
|
48
|
+
// The foundation remains a fixed import-only entry, not an extra styling layer.
|
|
49
|
+
const foundationEntry = path.join(dsRoot, config.stylesEntry);
|
|
50
|
+
try {
|
|
51
|
+
const css = postcss.parse(readFileSync(foundationEntry, "utf8"), { from: foundationEntry });
|
|
52
|
+
const imports = css.nodes.filter((node) => node.type !== "comment");
|
|
53
|
+
const expected = foundationFiles.filter((name) => name !== "fonts.css" || existsSync(path.join(dsRoot, config.tokenDir, name))).map((name) => path.join(dsRoot, config.tokenDir, name));
|
|
54
|
+
const actual = imports.map((node) => {
|
|
55
|
+
if (node.type !== "atrule" || node.name !== "import" || node.nodes) return null;
|
|
56
|
+
const parts = valueParser(node.params).nodes.filter((part) => part.type !== "space" && part.type !== "comment");
|
|
57
|
+
if (parts.length !== 1) return null;
|
|
58
|
+
const part = parts[0];
|
|
59
|
+
const value = part.type === "string" ? part.value : part.type === "function" && part.value === "url" && part.nodes.length === 1 ? part.nodes[0].value : null;
|
|
60
|
+
return value ? path.resolve(path.dirname(foundationEntry), value) : null;
|
|
61
|
+
});
|
|
62
|
+
if (actual.length !== expected.length || actual.some((file, index) => file !== expected[index] || !existsSync(file))) report(foundationEntry, "styles-entry", "Foundation CSS must import only fonts (when present), colors, typography, spacing, elevation, and base, in that order.");
|
|
63
|
+
} catch (error) { report(foundationEntry, "css-syntax", error.reason ?? error.message, error.line ?? 1); }
|
|
64
|
+
function checkCss(file, text, offset = { line: 1, column: 1 }) {
|
|
65
|
+
let css;
|
|
66
|
+
try { css = postcss.parse(text, { from: file }); }
|
|
67
|
+
catch (error) { report(file, "css-syntax", error.reason ?? error.message, offset.line + (error.line ?? 1) - 1); return; }
|
|
68
|
+
if (isDevelopmentFile(file, options) || !isInside(path.join(dsRoot, "components"), file) && !isInside(path.join(dsRoot, "patterns"), file) && file !== foundationEntry) report(file, "style-ownership", "Visual CSS belongs to the local ADS's runtime components or patterns.");
|
|
69
|
+
if (isDevelopmentFile(file, options) && css.nodes.some((node) => node.type !== "comment" && !(node.type === "atrule" && node.name === "import"))) report(file, "showcase-style", "Scenarios and fixtures define no styles. Put the frame/layout in a public ADS pattern and product UI in ADS components or patterns.", offset.line);
|
|
70
|
+
css.walkAtRules("import", (rule) => {
|
|
71
|
+
const first = valueParser(rule.params).nodes.find((node) => node.type !== "space" && node.type !== "comment");
|
|
72
|
+
const specifier = first?.type === "string" ? first.value : first?.type === "function" && first.value === "url" ? first.nodes[0]?.value : null;
|
|
73
|
+
if (!specifier || /^(?:[a-z]+:|\/\/)/i.test(specifier)) return;
|
|
74
|
+
const line = offset.line + rule.source.start.line - 1;
|
|
75
|
+
if (importViolation(specifier, file, options, resolveImport)) report(file, "public-import", `Import through ${config.publicAlias}, not ADS implementation paths.`, line);
|
|
76
|
+
if (showcaseImportViolation(specifier, file, options, resolveImport)) report(file, "showcase-import", "Showcase code and styles are development fixtures; runtime code must not import them.", line);
|
|
77
|
+
});
|
|
78
|
+
css.walkRules((rule) => {
|
|
79
|
+
if (isInside(path.join(dsRoot, "showcase"), file) && /:(?:hover|active|focus(?:-visible|-within)?|checked|disabled|enabled)\b|\[(?:aria-(?:selected|pressed|checked|disabled|expanded)|data-state)\b/.test(rule.selector)) report(file, "showcase-state", "Interactive state styling belongs to the real ADS component or pattern, not showcase or mock styles.", offset.line + rule.source.start.line - 1);
|
|
80
|
+
if (/\.ads-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "components"), file)) report(file, "style-ownership", "ADS component internals are owned by components, not app or showcase CSS.", offset.line + rule.source.start.line - 1);
|
|
81
|
+
if (/\.ux-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "patterns"), file)) report(file, "style-ownership", "Pattern internals are owned by patterns, not app or showcase CSS.", offset.line + rule.source.start.line - 1);
|
|
82
|
+
if (/\.showcase-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "showcase"), file)) report(file, "style-ownership", "Showcase internals are owned by showcase scaffolding.", offset.line + rule.source.start.line - 1);
|
|
83
|
+
});
|
|
84
|
+
css.walkDecls((decl) => {
|
|
85
|
+
const variable = checkComponentVariable(decl.prop, file, options);
|
|
86
|
+
if (variable) report(file, variable.rule, variable.message, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
87
|
+
for (const ref of references(decl.value)) {
|
|
88
|
+
const local = /^(?:--ds-|--ux-|--showcase-)/.test(ref.name);
|
|
89
|
+
if (!local && !isSharedToken(ref.name)) report(file, "token-name", `${ref.name} does not follow ADS's fixed token naming contract.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
90
|
+
else if (!local && !declaredTokens.has(ref.name) && !ref.fallback) report(file, "token-reference", `Undefined shared token ${ref.name}.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
91
|
+
}
|
|
92
|
+
const violations = new Set();
|
|
93
|
+
valueParser(decl.value).walk((node) => {
|
|
94
|
+
if (node.type === "function" && node.value === "url") return false;
|
|
95
|
+
if (node.type === "word" && /^(?:#[\da-f]{3,8}|[-+]?(?:\d*\.)?\d+(?:px|rem|em|m?s))$/i.test(node.value)) violations.add("Use a token for colors, dimensions, and durations.");
|
|
96
|
+
if (node.type === "function" && /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color|color-mix|cubic-bezier)$/.test(node.value)) violations.add("Define color formulas and easing in tokens.");
|
|
97
|
+
if (node.type === "function" && /^(?:blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|saturate|sepia)$/.test(node.value)) violations.add("Define visual filter effects in tokens.");
|
|
98
|
+
if (node.type === "word" && /^ease(?:-in|-out|-in-out)?$/.test(node.value)) violations.add("Use a motion token for easing.");
|
|
99
|
+
if (node.type === "word" && /^(?:color|background-color|(?:border|outline|caret|text-decoration|column-rule)(?:-[a-z]+)?-color|fill|stroke)$/.test(decl.prop) && /^[a-z]+$/i.test(node.value) && !/^(?:inherit|initial|unset|revert|currentcolor|transparent|none)$/i.test(node.value)) violations.add("Use a semantic color token.");
|
|
100
|
+
});
|
|
101
|
+
if (/^(?:font-family|font-weight|z-index)$/.test(decl.prop) && !/^(?:var\(|inherit$|auto$)/.test(decl.value)) violations.add("Use a semantic typography or layering token.");
|
|
102
|
+
for (const message of violations) report(file, "style-token", message, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) continue;
|
|
107
|
+
if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
|
|
108
|
+
if (file.endsWith(".css")) {
|
|
109
|
+
const text = readFileSync(file, "utf8");
|
|
110
|
+
if (file === path.join(sourceRoot, "app-mount.css") && isMountCss(text)) continue;
|
|
111
|
+
checkCss(file, text); continue;
|
|
112
|
+
}
|
|
113
|
+
const source = program.getSourceFile(file);
|
|
114
|
+
if (!source) continue;
|
|
115
|
+
for (const diagnostic of program.getSyntacticDiagnostics(source)) {
|
|
116
|
+
const position = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
117
|
+
report(file, "source-syntax", ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), position.line + 1, position.character + 1);
|
|
118
|
+
}
|
|
119
|
+
const inlineAllowed = config.inlineStyleFiles.some((allowed) => path.resolve(root, allowed) === file);
|
|
120
|
+
const showcase = isDevelopmentFile(file, options) && !isTestFile(file);
|
|
121
|
+
function nodeReport(node, rule, message) {
|
|
122
|
+
const position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
123
|
+
report(file, rule, message, position.line + 1, position.character + 1);
|
|
124
|
+
}
|
|
125
|
+
if (!showcase && (isInside(path.join(dsRoot, "components"), file) || isInside(path.join(dsRoot, "patterns"), file))) inspectPublicProps(source, checker, nodeReport);
|
|
126
|
+
function isLinariaTag(tag) {
|
|
127
|
+
if (ts.isCallExpression(tag)) tag = tag.expression;
|
|
128
|
+
let symbolNode = tag;
|
|
129
|
+
while (ts.isPropertyAccessExpression(symbolNode)) symbolNode = symbolNode.expression;
|
|
130
|
+
const symbol = checker.getSymbolAtLocation(symbolNode);
|
|
131
|
+
return symbol?.declarations?.some((decl) => {
|
|
132
|
+
if (ts.isImportSpecifier(decl)) {
|
|
133
|
+
const module = decl.parent.parent.parent.moduleSpecifier;
|
|
134
|
+
return ["css", "styled"].includes((decl.propertyName ?? decl.name).text) && ts.isStringLiteral(module) && module.text.startsWith("@linaria/");
|
|
135
|
+
}
|
|
136
|
+
if (ts.isNamespaceImport(decl) && ts.isPropertyAccessExpression(tag) && /^(?:css|styled)(?:\.|$)/.test(tag.getText(source).slice(symbolNode.getText(source).length + 1))) {
|
|
137
|
+
const module = decl.parent.parent.moduleSpecifier;
|
|
138
|
+
return ts.isStringLiteral(module) && module.text.startsWith("@linaria/");
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function inspectProperty(value) {
|
|
144
|
+
if (ts.isPropertyAssignment(value) && (ts.isStringLiteralLike(value.name) || ts.isIdentifier(value.name))) {
|
|
145
|
+
const variable = checkComponentVariable(value.name.text, file, options);
|
|
146
|
+
if (variable) nodeReport(value.name, variable.rule, variable.message);
|
|
147
|
+
}
|
|
148
|
+
ts.forEachChild(value, inspectProperty);
|
|
149
|
+
}
|
|
150
|
+
function inspectClass(value) {
|
|
151
|
+
if (ts.isStringLiteralLike(value)) {
|
|
152
|
+
if (/(?:^|\s)ads-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "components"), file)) nodeReport(value, "style-ownership", "Primitive ads-* classes must be emitted by components, not consumers.");
|
|
153
|
+
if (/(?:^|\s)ux-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "patterns"), file)) nodeReport(value, "style-ownership", "Pattern ux-* classes must be emitted by patterns, not consumers.");
|
|
154
|
+
if (/(?:^|\s)showcase-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "showcase"), file)) nodeReport(value, "style-ownership", "Showcase hooks must be emitted by the showcase, not runtime implementations.");
|
|
155
|
+
}
|
|
156
|
+
ts.forEachChild(value, inspectClass);
|
|
157
|
+
}
|
|
158
|
+
const nativePreact = (source.pragmas?.get("jsximportsource")?.arguments.factory ?? compiler.jsxImportSource) === "preact";
|
|
159
|
+
function checkSvgAttribute(node, name) {
|
|
160
|
+
if (svgPresentation.has(name)) nodeReport(node, "svg-attribute", `Native Preact SVG needs ${name.replace(/[A-Z]/g, letter => "-" + letter.toLowerCase())}; React-style ${name} is not normalized by its native renderer.`);
|
|
161
|
+
}
|
|
162
|
+
function visit(node) {
|
|
163
|
+
if (nativePreact && ts.isJsxAttribute(node) && ts.isIdentifier(node.name)) {
|
|
164
|
+
const parent = node.parent.parent;
|
|
165
|
+
if ((ts.isJsxOpeningElement(parent) || ts.isJsxSelfClosingElement(parent)) && ts.isIdentifier(parent.tagName) && svgTags.has(parent.tagName.text)) checkSvgAttribute(node, node.name.text);
|
|
166
|
+
}
|
|
167
|
+
if (ts.isCallExpression(node) && uiOperation(node.expression, checker) === "createElement") {
|
|
168
|
+
const [tag, props] = node.arguments;
|
|
169
|
+
const properties = props && ts.isObjectLiteralExpression(props) ? props.properties : [];
|
|
170
|
+
for (const property of properties) {
|
|
171
|
+
if (!ts.isPropertyAssignment(property) || !(ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name))) continue;
|
|
172
|
+
if (tag && ts.isStringLiteralLike(tag) && svgTags.has(tag.text) && importedUiApi(node.expression, checker)?.module === "preact") checkSvgAttribute(property, property.name.text);
|
|
173
|
+
if (property.name.text === "style" && (!inlineAllowed || showcase)) nodeReport(property, "no-inline-style", "UI factories follow the same style ownership as JSX. Use semantic ADS props or a narrow renderer boundary.");
|
|
174
|
+
if (property.name.text === "style") inspectProperty(property.initializer);
|
|
175
|
+
if (property.name.text === "className") inspectClass(property.initializer);
|
|
176
|
+
if (showcase && property.name.text === "className") nodeReport(property, "showcase-class", "Scenarios use public ADS props, not custom class names.");
|
|
177
|
+
}
|
|
178
|
+
if (showcase && tag && ts.isStringLiteralLike(tag)) {
|
|
179
|
+
const role = properties.find(property => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) && property.name.text === "role")?.initializer;
|
|
180
|
+
if (showcaseControls.has(tag.text) || role && ts.isStringLiteralLike(role) && widgetRoles.has(role.text)) nodeReport(node, "showcase-control", "Scenario controls must render public ADS components.");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (isInside(dsRoot, file) && (ts.isIdentifier(node) && ["fetch", "XMLHttpRequest", "WebSocket", "EventSource"].includes(node.text) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node))) {
|
|
184
|
+
const declarations = checker.getSymbolAtLocation(node)?.declarations ?? [];
|
|
185
|
+
if (!declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile)) nodeReport(node, "data-access", "ADS UI and scenarios receive business data through typed inputs or injected services. Direct network access belongs in an application adapter; fixture services remain local.");
|
|
186
|
+
}
|
|
187
|
+
if (isInside(dsRoot, file) && ts.isPropertyAccessExpression(node) && ["window", "globalThis", "self"].includes(node.expression.getText(source)) && ["fetch", "XMLHttpRequest", "WebSocket", "EventSource"].includes(node.name.text)) nodeReport(node, "data-access", "Inject the data service instead of using a global network API in ADS UI or scenarios.");
|
|
188
|
+
if (showcase && (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName) && /^[a-z]/.test(node.tagName.text)) {
|
|
189
|
+
const role = node.attributes.properties.find((attr) => ts.isJsxAttribute(attr) && attr.name.text === "role")?.initializer;
|
|
190
|
+
const roleValue = role && ts.isStringLiteralLike(role) ? role.text : role && ts.isJsxExpression(role) && role.expression && ts.isStringLiteralLike(role.expression) ? role.expression.text : null;
|
|
191
|
+
if (showcaseControls.has(node.tagName.text) || widgetRoles.has(roleValue)) nodeReport(node, "showcase-control", "Showcase and mock controls must use public ADS components. Implement missing reusable controls in the ADS, then import them here.");
|
|
192
|
+
}
|
|
193
|
+
if (showcase && ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className") nodeReport(node, "showcase-class", "Showcase and mocks use known public ADS entities and props, not custom class names. Put styling in the owning ADS component or frame pattern.");
|
|
194
|
+
let specifier;
|
|
195
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) specifier = node.moduleSpecifier.text;
|
|
196
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0] && ts.isStringLiteral(node.arguments[0])) specifier = node.arguments[0].text;
|
|
197
|
+
if (specifier && importViolation(specifier, file, options, resolveImport)) nodeReport(node, "public-import", `Import through ${config.publicAlias}, not ADS implementation paths.`);
|
|
198
|
+
if (specifier && resolveImport(specifier, file).some((target) => (specifier.startsWith(".") || existsSync(target)) && target.endsWith(".css") && isInside(root, target) && !isInside(dsRoot, target) && target !== path.join(sourceRoot, "app-mount.css") && !isInside(path.join(root, "node_modules"), target))) nodeReport(node, "style-ownership", "Imported project CSS belongs to ADS components, patterns, or the foundation. Only structural app-mount.css is allowed outside the ADS.");
|
|
199
|
+
if (specifier && appImportViolation(specifier, file, options, resolveImport)) nodeReport(node, "no-app-import", "ADS implementations and showcase fixtures must not import app code. Use public ADS APIs with local fixture data; keep application integration harnesses in application tests.");
|
|
200
|
+
if (specifier && showcaseImportViolation(specifier, file, options, resolveImport)) nodeReport(node, "showcase-import", "Showcase code and styles are development fixtures; runtime code must not import them.");
|
|
201
|
+
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && (!inlineAllowed || showcase)) nodeReport(node, "no-inline-style", "Move visual policy into the local ADS, or document a narrow measured-geometry exception in inlineStyleFiles.");
|
|
202
|
+
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && node.initializer) {
|
|
203
|
+
inspectProperty(node.initializer);
|
|
204
|
+
}
|
|
205
|
+
const mutation = styleWriteViolation(node, file, options);
|
|
206
|
+
if (mutation) nodeReport(node, "style-mutation", mutation);
|
|
207
|
+
if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "Do not expose CSSProperties-based visual plumbing. Use semantic props, or document a narrow JSX renderer boundary.");
|
|
208
|
+
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
|
|
209
|
+
inspectClass(node.initializer);
|
|
210
|
+
}
|
|
211
|
+
if (ts.isTaggedTemplateExpression(node) && isLinariaTag(node.tag)) {
|
|
212
|
+
const template = node.template;
|
|
213
|
+
const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text : template.head.text + template.templateSpans.map((span) => `__ds_expression__${span.literal.text}`).join("");
|
|
214
|
+
const position = source.getLineAndCharacterOfPosition(template.getStart(source) + 1);
|
|
215
|
+
checkCss(file, text, { line: position.line + 1, column: position.character + 1 });
|
|
216
|
+
}
|
|
217
|
+
ts.forEachChild(node, visit);
|
|
218
|
+
}
|
|
219
|
+
visit(source);
|
|
220
|
+
}
|
|
221
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
222
|
+
}
|