@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/cli.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { checkProject, generateBrandAssets, generateLintConfig, initProject, inspectTokens } from "./index.mjs";
|
|
5
|
+
|
|
6
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
7
|
+
const help = `ADS ${pkg.version} — tooling for a project's local design system
|
|
8
|
+
|
|
9
|
+
Usage: ads init [options] Run inside your project
|
|
10
|
+
ads <command> [directory] [--json]
|
|
11
|
+
|
|
12
|
+
init Set up ADS in the current project (requires package.json)
|
|
13
|
+
check [directory] Check tokens, ownership, imports, and showcase coverage
|
|
14
|
+
tokens [directory] Inspect local token values and theme definitions
|
|
15
|
+
assets [directory] Generate standard web icons in .ds/assets from assets.iconSource
|
|
16
|
+
lint-config [dir] Generate .ds/oxlintrc.json with core and project plugins
|
|
17
|
+
mcp [directory] Serve stdio MCP tools/Apps and the local showcase website
|
|
18
|
+
showcase [directory] Serve the built-in MCP client website (no stdio connection)
|
|
19
|
+
|
|
20
|
+
--help Show this help
|
|
21
|
+
--version Show the tool version
|
|
22
|
+
--framework name Init only: react or preact (required until saved in config)
|
|
23
|
+
--tool-package spec Init only: tool version or local tarball for devDependency
|
|
24
|
+
--package-manager pm Init only: npm or bun (auto-detected from the project)
|
|
25
|
+
--port number MCP/showcase only: loopback website port (default: automatic)
|
|
26
|
+
--no-showcase MCP only: disable the local website
|
|
27
|
+
--mutations MCP only: enable guarded ADS source editing
|
|
28
|
+
|
|
29
|
+
External components and examples are imported using the ADS skills.
|
|
30
|
+
No component library or example catalogue is bundled.
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
async function main(args) {
|
|
34
|
+
if (!args.length || args.includes("--help")) { process.stdout.write(help); return; }
|
|
35
|
+
if (args.length === 1 && args[0] === "--version") { console.log(pkg.version); return; }
|
|
36
|
+
let json = false;
|
|
37
|
+
let toolPackage;
|
|
38
|
+
let packageManager;
|
|
39
|
+
let framework;
|
|
40
|
+
let port;
|
|
41
|
+
let showcase = true;
|
|
42
|
+
let mutations = false;
|
|
43
|
+
const positional = [];
|
|
44
|
+
for (let index = 0; index < args.length; index++) {
|
|
45
|
+
const argument = args[index];
|
|
46
|
+
if (argument === "--json") json = true;
|
|
47
|
+
else if (argument === "--no-showcase") showcase = false;
|
|
48
|
+
else if (argument === "--mutations") mutations = true;
|
|
49
|
+
else if (argument === "--port") {
|
|
50
|
+
const value = args[++index];
|
|
51
|
+
if (port !== undefined || !/^\d+$/.test(value ?? "") || Number(value) > 65535) throw new Error("--port requires one port between 0 and 65535.");
|
|
52
|
+
port = Number(value);
|
|
53
|
+
}
|
|
54
|
+
else if (argument === "--tool-package") {
|
|
55
|
+
if (toolPackage !== undefined || !args[index + 1] || args[index + 1].startsWith("-")) throw new Error("Specify one tool version or local tarball after --tool-package.");
|
|
56
|
+
toolPackage = args[++index];
|
|
57
|
+
} else if (argument === "--package-manager") {
|
|
58
|
+
if (packageManager !== undefined || !args[index + 1] || args[index + 1].startsWith("-")) throw new Error("Specify one package manager after --package-manager: npm or bun.");
|
|
59
|
+
packageManager = args[++index];
|
|
60
|
+
} else if (argument === "--framework") {
|
|
61
|
+
if (framework !== undefined || !args[index + 1] || args[index + 1].startsWith("-")) throw new Error("Specify one framework after --framework: react or preact.");
|
|
62
|
+
framework = args[++index];
|
|
63
|
+
} else if (argument.startsWith("-")) throw new Error("Unknown option. Run ads --help.");
|
|
64
|
+
else positional.push(argument);
|
|
65
|
+
}
|
|
66
|
+
if (positional.length > 2) throw new Error("Extra argument. Run ads --help.");
|
|
67
|
+
const [command, directory] = positional;
|
|
68
|
+
if ((port !== undefined || !showcase) && !["mcp", "showcase"].includes(command)) throw new Error("Website options are supported only by mcp/showcase.");
|
|
69
|
+
if (!showcase && command !== "mcp") throw new Error("--no-showcase is supported only by mcp.");
|
|
70
|
+
if (mutations && command !== "mcp") throw new Error("--mutations is supported only by mcp.");
|
|
71
|
+
if (toolPackage !== undefined && command !== "init") throw new Error("--tool-package is supported only by init.");
|
|
72
|
+
if (packageManager !== undefined && command !== "init") throw new Error("--package-manager is supported only by init.");
|
|
73
|
+
if (framework !== undefined && command !== "init") throw new Error("--framework is supported only by init.");
|
|
74
|
+
if (command === "init" && directory) throw new Error("Run ads init inside the project, without a directory argument.");
|
|
75
|
+
const target = path.resolve(directory ?? process.cwd());
|
|
76
|
+
if (command === "mcp" || command === "showcase") {
|
|
77
|
+
if (json) throw new Error("--json is not supported by mcp/showcase; mcp reserves stdout for JSON-RPC.");
|
|
78
|
+
const { startAdsMcp } = await import("./mcp/server.mjs");
|
|
79
|
+
await startAdsMcp(target, { stdio: command === "mcp", showcase, port, mutations });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let result;
|
|
83
|
+
if (command === "init") result = initProject(target, { toolPackage, packageManager, framework });
|
|
84
|
+
else if (command === "check") result = checkProject(target);
|
|
85
|
+
else if (command === "tokens") result = inspectTokens(target);
|
|
86
|
+
else if (command === "assets") result = await generateBrandAssets(target);
|
|
87
|
+
else if (command === "lint-config") result = generateLintConfig(target);
|
|
88
|
+
else throw new Error(`Unknown command: ${command}. Run ads --help.`);
|
|
89
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
90
|
+
else if (command === "init") console.log(`Initialized ${target}\nFramework: ${result.framework}\nAdded ${result.toolDependency.name} (${result.toolDependency.spec}) as a development dependency.\nRun ${result.packageManager} install and ${result.packageManager} run build; the build refreshes lint configuration, runs ADS checks, and generates configured brand assets automatically.`);
|
|
91
|
+
else if (command === "assets") console.log(result.skipped ? "No assets.iconSource configured; brand generation skipped." : `Generated ${result.files.length} brand assets in ${result.directory}.`);
|
|
92
|
+
else if (command === "lint-config") console.log(`Generated ${result.file}\nProjects initialized with ads init refresh this automatically during lint:ds/build.`);
|
|
93
|
+
else if (command === "tokens") {
|
|
94
|
+
for (const token of result.tokens) console.log(`${token.name}${token.type ? ` (${token.type})` : ""}: ${token.definitions.map((entry) => `${entry.scope}: ${entry.value}`).join("; ")}`);
|
|
95
|
+
} else if (result.ok) console.log(`ADS check passed: ${target}`);
|
|
96
|
+
if (!json && result.diagnostics?.length) {
|
|
97
|
+
for (const diagnostic of result.diagnostics) console.error(`${diagnostic.file}:${diagnostic.line}:${diagnostic.column} [${diagnostic.rule}] ${diagnostic.message}`);
|
|
98
|
+
}
|
|
99
|
+
if (result.ok === false || result.diagnostics?.length) process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
103
|
+
if (process.argv.includes("--json")) console.log(JSON.stringify({ ok: false, error: error.message }));
|
|
104
|
+
else console.error(`ads: ${error.message}`);
|
|
105
|
+
process.exitCode = 2;
|
|
106
|
+
});
|
package/dist/config.mjs
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isSharedToken } from "./token-policy.mjs";
|
|
4
|
+
|
|
5
|
+
export const supportedFrameworks = ["react", "preact"];
|
|
6
|
+
|
|
7
|
+
export const defaultConfig = {
|
|
8
|
+
schemaVersion: 1,
|
|
9
|
+
framework: null,
|
|
10
|
+
sourceDir: "src",
|
|
11
|
+
dsDir: "design-system",
|
|
12
|
+
publicAlias: "@ds",
|
|
13
|
+
publicEntry: "index.ts",
|
|
14
|
+
stylesEntry: "styles.css",
|
|
15
|
+
tokenDir: "tokens",
|
|
16
|
+
inlineStyleFiles: [],
|
|
17
|
+
boundaries: { styleWrites: [], componentVariables: [] },
|
|
18
|
+
assets: { publicDirs: ["public/assets"], generatedPaths: ["dist", "dist-showcase", ".vite", ".ds"], generatedFiles: [], iconSource: null },
|
|
19
|
+
oxlint: { plugins: [], rules: {}, overrides: [] },
|
|
20
|
+
mcp: { showcaseUrl: null, showcaseTitle: null, apps: [], previewWatch: null, previewManifest: null, mutationChecks: null },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function isInside(parent, file) {
|
|
24
|
+
const relative = path.relative(parent, file);
|
|
25
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function projectPath(root, value, field) {
|
|
29
|
+
if (typeof value !== "string" || !value.trim() || path.isAbsolute(value)) {
|
|
30
|
+
throw new Error(`${field} must be a non-empty relative path.`);
|
|
31
|
+
}
|
|
32
|
+
const absolute = path.resolve(root, value);
|
|
33
|
+
if (!isInside(root, absolute)) throw new Error(`${field} must stay inside ${root}.`);
|
|
34
|
+
return absolute;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadConfig(directory = process.cwd()) {
|
|
38
|
+
const root = path.resolve(directory);
|
|
39
|
+
const configFile = path.join(root, "ds.config.json");
|
|
40
|
+
const user = existsSync(configFile) ? JSON.parse(readFileSync(configFile, "utf8")) : {};
|
|
41
|
+
if (!user || typeof user !== "object" || Array.isArray(user)) throw new Error("ds.config.json must be an object.");
|
|
42
|
+
for (const field of Object.keys(user)) {
|
|
43
|
+
if (!(field in defaultConfig)) throw new Error(`Unknown ADS configuration field: ${field}`);
|
|
44
|
+
}
|
|
45
|
+
if (user.oxlint !== undefined && (!user.oxlint || typeof user.oxlint !== "object" || Array.isArray(user.oxlint))) throw new Error("oxlint must be an object.");
|
|
46
|
+
for (const field of ["boundaries", "assets", "oxlint", "mcp"]) {
|
|
47
|
+
if (user[field] !== undefined && (!user[field] || typeof user[field] !== "object" || Array.isArray(user[field]))) throw new Error(`${field} must be an object.`);
|
|
48
|
+
for (const key of Object.keys(user[field] ?? {})) if (!(key in defaultConfig[field])) throw new Error(`Unknown ${field} configuration field: ${key}`);
|
|
49
|
+
}
|
|
50
|
+
const config = { ...defaultConfig, ...user, boundaries: { ...defaultConfig.boundaries, ...user.boundaries }, assets: { ...defaultConfig.assets, ...user.assets }, oxlint: { ...defaultConfig.oxlint, ...user.oxlint }, mcp: { ...defaultConfig.mcp, ...user.mcp } };
|
|
51
|
+
if (config.schemaVersion !== 1) throw new Error(`Unsupported ADS config version: ${config.schemaVersion}`);
|
|
52
|
+
if (user.framework !== undefined && !supportedFrameworks.includes(user.framework)) throw new Error("framework must be react or preact.");
|
|
53
|
+
if (typeof config.publicAlias !== "string" || !config.publicAlias.trim()) throw new Error("publicAlias must be a non-empty string.");
|
|
54
|
+
if (!Array.isArray(config.inlineStyleFiles)) throw new Error("inlineStyleFiles must be an array of relative paths.");
|
|
55
|
+
const sourceRoot = projectPath(root, config.sourceDir, "sourceDir");
|
|
56
|
+
const dsRoot = projectPath(root, config.dsDir, "dsDir");
|
|
57
|
+
for (const field of ["publicEntry", "stylesEntry", "tokenDir"]) projectPath(dsRoot, config[field], field);
|
|
58
|
+
for (const file of config.inlineStyleFiles) projectPath(root, file, "inlineStyleFiles");
|
|
59
|
+
if (!Array.isArray(config.boundaries.componentVariables) || config.boundaries.componentVariables.some((name) => typeof name !== "string" || !/^--ds-[\w-]+$/.test(name) || isStateVariable(name))) throw new Error("boundaries.componentVariables must name non-state --ds-* customization variables.");
|
|
60
|
+
if (!Array.isArray(config.boundaries.styleWrites)) throw new Error("boundaries.styleWrites must be an array.");
|
|
61
|
+
const boundaryFiles = new Set();
|
|
62
|
+
for (const boundary of config.boundaries.styleWrites) {
|
|
63
|
+
if (!boundary || typeof boundary !== "object" || Array.isArray(boundary) || Object.keys(boundary).some((key) => !["file", "properties"].includes(key))) throw new Error("Each styleWrites boundary requires file and properties.");
|
|
64
|
+
const file = projectPath(root, boundary.file, "boundaries.styleWrites.file");
|
|
65
|
+
if (boundaryFiles.has(file)) throw new Error("Duplicate styleWrites file boundary.");
|
|
66
|
+
boundaryFiles.add(file);
|
|
67
|
+
if (!Array.isArray(boundary.properties) || !boundary.properties.length || boundary.properties.some((name) => typeof name !== "string" || !/^(?:--[\w-]+|[a-z][a-z0-9-]*)$/.test(name) || name === "css-text" || isStateVariable(name) || isSharedToken(name) || name.startsWith("--") && !/^(?:--ds-|--ux-|--showcase-)/.test(name))) throw new Error("styleWrites.properties must name explicit CSS properties; bulk cssText, shared tokens, foreign prefixes, and primitive-state writes cannot be allowed.");
|
|
68
|
+
}
|
|
69
|
+
for (const field of ["publicDirs", "generatedPaths", "generatedFiles"]) {
|
|
70
|
+
if (!Array.isArray(config.assets[field])) throw new Error(`assets.${field} must be an array.`);
|
|
71
|
+
for (const entry of config.assets[field]) {
|
|
72
|
+
const base = field === "generatedFiles" ? path.join(dsRoot, "assets") : root;
|
|
73
|
+
const absolute = projectPath(base, entry, `assets.${field}`);
|
|
74
|
+
if (absolute === base) throw new Error(`assets.${field} must name a path below its root.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(config.oxlint.plugins) || !config.oxlint.rules || typeof config.oxlint.rules !== "object" || Array.isArray(config.oxlint.rules)) {
|
|
78
|
+
throw new Error("oxlint requires plugins (array) and rules (object).");
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(config.oxlint.overrides)) throw new Error("oxlint.overrides must be an array.");
|
|
81
|
+
for (const override of config.oxlint.overrides) {
|
|
82
|
+
if (!override || typeof override !== "object" || Array.isArray(override) || Object.keys(override).some((key) => !["files", "rules"].includes(key))) throw new Error("Each oxlint override requires files and rules only.");
|
|
83
|
+
if (!Array.isArray(override.files) || !override.files.length || override.files.some((file) => typeof file !== "string" || !file.trim() || file.startsWith("!") || path.isAbsolute(file) || file.split(/[\\/]/).includes(".."))) throw new Error("oxlint.overrides.files must contain project-relative paths or globs.");
|
|
84
|
+
if (!override.rules || typeof override.rules !== "object" || Array.isArray(override.rules)) throw new Error("oxlint.overrides.rules must be an object.");
|
|
85
|
+
}
|
|
86
|
+
if (config.assets.iconSource !== null) {
|
|
87
|
+
const source = projectPath(dsRoot, config.assets.iconSource, "assets.iconSource");
|
|
88
|
+
if (!isInside(path.join(dsRoot, "assets/brand"), source) || path.extname(source).toLowerCase() !== ".svg") throw new Error("assets.iconSource must name an SVG inside the ADS's assets/brand/ folder.");
|
|
89
|
+
}
|
|
90
|
+
for (const plugin of config.oxlint.plugins) {
|
|
91
|
+
if (!plugin || Object.keys(plugin).some((key) => !["name", "path"].includes(key)) || typeof plugin.name !== "string" || !plugin.name || typeof plugin.path !== "string") {
|
|
92
|
+
throw new Error("Each project Oxlint plugin needs a name and a relative path.");
|
|
93
|
+
}
|
|
94
|
+
projectPath(root, plugin.path, "oxlint.plugins.path");
|
|
95
|
+
}
|
|
96
|
+
if (config.mcp.showcaseUrl !== null) {
|
|
97
|
+
let url;
|
|
98
|
+
try { url = new URL(config.mcp.showcaseUrl); } catch { throw new Error("mcp.showcaseUrl must be an http(s) URL or null."); }
|
|
99
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error("mcp.showcaseUrl must be an http(s) URL without credentials.");
|
|
100
|
+
}
|
|
101
|
+
if (config.mcp.showcaseTitle !== null && (typeof config.mcp.showcaseTitle !== "string" || !config.mcp.showcaseTitle.trim() || config.mcp.showcaseTitle.length > 120)) throw new Error("mcp.showcaseTitle must be a non-empty title of at most 120 characters or null.");
|
|
102
|
+
if (!Array.isArray(config.mcp.apps)) throw new Error("mcp.apps must be an array of built project previews.");
|
|
103
|
+
if (config.mcp.mutationChecks !== null && (!Array.isArray(config.mcp.mutationChecks) || config.mcp.mutationChecks.length > 12 || new Set(config.mcp.mutationChecks).size !== config.mcp.mutationChecks.length || config.mcp.mutationChecks.some(name => typeof name !== "string" || !/^[a-zA-Z0-9][\w:.-]*$/.test(name)))) throw new Error("mcp.mutationChecks must be null or up to 12 unique package script names.");
|
|
104
|
+
if (config.mcp.previewManifest !== null) {
|
|
105
|
+
const manifest = projectPath(root, config.mcp.previewManifest, "mcp.previewManifest");
|
|
106
|
+
if (!isInside(path.join(root, ".ds"), manifest) || path.extname(manifest) !== ".json") throw new Error("mcp.previewManifest must name a generated JSON manifest inside .ds/.");
|
|
107
|
+
}
|
|
108
|
+
if (config.mcp.previewWatch !== null) {
|
|
109
|
+
const watch = config.mcp.previewWatch;
|
|
110
|
+
if (!watch || typeof watch !== "object" || Array.isArray(watch) || Object.keys(watch).some((key) => !["command", "args"].includes(key)) || typeof watch.command !== "string" || !watch.command.trim() || watch.command.includes("\0") || !Array.isArray(watch.args) || watch.args.some((arg) => typeof arg !== "string" || arg.includes("\0"))) throw new Error("mcp.previewWatch requires command (executable) and args (string array), or null.");
|
|
111
|
+
}
|
|
112
|
+
const appIds = new Set();
|
|
113
|
+
for (const app of config.mcp.apps) {
|
|
114
|
+
if (!app || typeof app !== "object" || Object.keys(app).some((key) => !["id", "title", "kind", "file", "description"].includes(key)) || !/^[a-z][a-z0-9-]{0,63}$/.test(app.id) || app.id === "tokens" || appIds.has(app.id) || typeof app.title !== "string" || !app.title.trim() || !["components", "patterns", "showcase"].includes(app.kind) || app.description !== undefined && typeof app.description !== "string") throw new Error("Each mcp.apps entry needs a unique id (tokens is reserved), title, kind (components/patterns/showcase), and project-relative HTML file.");
|
|
115
|
+
const file = projectPath(root, app.file, "mcp.apps.file");
|
|
116
|
+
if (path.extname(file) !== ".html") throw new Error("mcp.apps.file must be a built, self-contained HTML file.");
|
|
117
|
+
appIds.add(app.id);
|
|
118
|
+
}
|
|
119
|
+
return { root, config, sourceRoot, dsRoot };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isStateVariable(name) {
|
|
123
|
+
return /^--ds-[\w-]+-(?:sel|selected|hover|active|focus|off|disabled)(?:-|$)/.test(name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isTestFile(file) {
|
|
127
|
+
return /(?:\.(?:test|spec)\.[cm]?[jt]sx?$|[\\/](?:test|tests|__tests__)[\\/])/.test(file);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isScenarioFile(file) {
|
|
131
|
+
return /(?:^|[\\/])Scenario\.[cm]?[jt]sx?$/.test(file) || /\.scenario\.[cm]?[jt]sx?$/.test(file);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isFixtureFile(file) {
|
|
135
|
+
return /(?:^|[\\/])fixtures?\.(?:[cm]?[jt]sx?|json)$/.test(file) || /\.fixtures?\.(?:[cm]?[jt]sx?|json)$/.test(file);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** File role, rather than folder location, keeps colocated preview code private. */
|
|
139
|
+
export function isDevelopmentFile(file, { dsRoot }) {
|
|
140
|
+
if (!isInside(dsRoot, file)) return isTestFile(file);
|
|
141
|
+
const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
|
|
142
|
+
return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file)
|
|
143
|
+
|| /^(?:showcase|composition)\//.test(relative)
|
|
144
|
+
|| /(?:^|\/)(?:scenario|fixtures)(?:\/|$)/.test(relative)
|
|
145
|
+
|| /\.scenario(?:-adapter)?\.[cm]?[jt]sx?$/.test(relative);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ignored = new Set(["node_modules", "dist", "dist-showcase", "dist-kit", ".git", ".vite", ".ds", "coverage"]);
|
|
149
|
+
|
|
150
|
+
export function sourceFiles(root) {
|
|
151
|
+
if (!existsSync(root)) return [];
|
|
152
|
+
const files = [];
|
|
153
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
154
|
+
if (ignored.has(entry.name) || entry.isSymbolicLink()) continue;
|
|
155
|
+
const file = path.join(root, entry.name);
|
|
156
|
+
if (entry.isDirectory()) files.push(...sourceFiles(file));
|
|
157
|
+
else if (entry.isFile() && /\.(css|html|[cm]?[jt]sx?)$/.test(entry.name)) files.push(file);
|
|
158
|
+
}
|
|
159
|
+
return files.sort();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function importViolation(specifier, filename, options, resolve) {
|
|
163
|
+
const { root, dsRoot, config } = options;
|
|
164
|
+
// ADS implementation code needs its private imports; the showcase is a consumer.
|
|
165
|
+
if (isInside(dsRoot, filename) && !isDevelopmentFile(filename, options)) return false;
|
|
166
|
+
if (specifier.startsWith(`${config.publicAlias}/`)) return true;
|
|
167
|
+
if (specifier === config.publicAlias) return false;
|
|
168
|
+
const imported = resolve ? resolve(specifier, filename)
|
|
169
|
+
: specifier.startsWith(".") ? [path.resolve(path.dirname(filename), specifier)]
|
|
170
|
+
: specifier.startsWith(`${config.dsDir}/`) ? [path.resolve(root, specifier)] : [];
|
|
171
|
+
return imported.some((file) => {
|
|
172
|
+
if (!isInside(dsRoot, file)) return false;
|
|
173
|
+
// Owner-local white-box tests may exercise private helpers without making
|
|
174
|
+
// those helpers public API. Scenarios remain public-API consumers.
|
|
175
|
+
if (isTestFile(filename) && isInside(dsRoot, filename) && isInside(path.dirname(filename), file)) return false;
|
|
176
|
+
if (isDevelopmentFile(filename, options) && isDevelopmentFile(file, options)) return false;
|
|
177
|
+
if (isTestFile(filename) && (isInside(path.join(dsRoot, "showcase"), file) || isTestFile(file))) return false;
|
|
178
|
+
if (isInside(path.join(dsRoot, "showcase"), filename) && isInside(path.join(dsRoot, "showcase"), file)) return false;
|
|
179
|
+
return file !== path.join(dsRoot, config.stylesEntry);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"description": "Shared CSS token names, semantics, and value categories. Values belong to the project.",
|
|
4
|
+
"naming": {
|
|
5
|
+
"prefixes": ["--color-", "--slate-", "--warm-", "--rose-", "--bg-", "--text-", "--border-", "--font-", "--weight-", "--leading-", "--tracking-", "--type-", "--space-", "--radius-", "--control-", "--icon-", "--shadow-", "--ring-", "--filter-", "--ease-", "--dur-", "--z-", "--accent-", "--focus-", "--scroll-", "--success-", "--warning-", "--danger-", "--info-", "--side-pane-", "--toast-"],
|
|
6
|
+
"names": ["--accent", "--success", "--warning", "--danger", "--info", "--inset-hairline", "--navigation-rail-width", "--app-header-height", "--content-max", "--row-height-compact", "--compact-column-min", "--stack-gap-compact"]
|
|
7
|
+
},
|
|
8
|
+
"tokens": {
|
|
9
|
+
"--bg-canvas": { "type": "color", "meaning": "Application canvas" },
|
|
10
|
+
"--bg-surface": { "type": "color", "meaning": "Component surface" },
|
|
11
|
+
"--bg-hover": { "type": "color", "meaning": "Hovered surface" },
|
|
12
|
+
"--text-strong": { "type": "color", "meaning": "Strong text" },
|
|
13
|
+
"--text-body": { "type": "color", "meaning": "Body text" },
|
|
14
|
+
"--text-muted": { "type": "color", "meaning": "Secondary text" },
|
|
15
|
+
"--text-inverse": { "type": "color", "meaning": "Text on contrasting fills" },
|
|
16
|
+
"--border-default": { "type": "color", "meaning": "Default border" },
|
|
17
|
+
"--border-focus": { "type": "color", "meaning": "Focus border" },
|
|
18
|
+
"--accent": { "type": "color", "meaning": "Brand accent" },
|
|
19
|
+
"--accent-hover": { "type": "color", "meaning": "Hovered brand accent" },
|
|
20
|
+
"--accent-fg": { "type": "color", "meaning": "Foreground on accent" },
|
|
21
|
+
"--font-sans": { "type": "fontFamily", "meaning": "UI font stack" },
|
|
22
|
+
"--font-mono": { "type": "fontFamily", "meaning": "Code font stack" },
|
|
23
|
+
"--text-sm": { "type": "dimension", "meaning": "Small UI text" },
|
|
24
|
+
"--text-base": { "type": "dimension", "meaning": "Body text size" },
|
|
25
|
+
"--text-lg": { "type": "dimension", "meaning": "Heading text size" },
|
|
26
|
+
"--text-xl": { "type": "dimension", "meaning": "Large heading text size" },
|
|
27
|
+
"--weight-normal": { "type": "number", "meaning": "Normal font weight" },
|
|
28
|
+
"--weight-medium": { "type": "number", "meaning": "Medium font weight" },
|
|
29
|
+
"--weight-semibold": { "type": "number", "meaning": "Semibold font weight" },
|
|
30
|
+
"--leading-tight": { "type": "number", "meaning": "Tight line height" },
|
|
31
|
+
"--leading-normal": { "type": "number", "meaning": "Normal line height" },
|
|
32
|
+
"--space-0": { "type": "dimension", "meaning": "Zero spacing" },
|
|
33
|
+
"--space-2": { "type": "dimension", "meaning": "Spacing step 2" },
|
|
34
|
+
"--space-4": { "type": "dimension", "meaning": "Spacing step 4" },
|
|
35
|
+
"--space-6": { "type": "dimension", "meaning": "Spacing step 6" },
|
|
36
|
+
"--space-8": { "type": "dimension", "meaning": "Spacing step 8" },
|
|
37
|
+
"--space-12": { "type": "dimension", "meaning": "Spacing step 12" },
|
|
38
|
+
"--space-16": { "type": "dimension", "meaning": "Spacing step 16" },
|
|
39
|
+
"--radius-sm": { "type": "dimension", "meaning": "Small radius" },
|
|
40
|
+
"--radius-md": { "type": "dimension", "meaning": "Medium radius" },
|
|
41
|
+
"--radius-lg": { "type": "dimension", "meaning": "Large radius" },
|
|
42
|
+
"--border-w": { "type": "dimension", "meaning": "Default border width" },
|
|
43
|
+
"--focus-w": { "type": "dimension", "meaning": "Focus ring width" },
|
|
44
|
+
"--focus-offset": { "type": "dimension", "meaning": "Focus ring offset" },
|
|
45
|
+
"--shadow-xs": { "type": "shadow", "meaning": "Subtle shadow" },
|
|
46
|
+
"--shadow-raised": { "type": "shadow", "meaning": "Raised surface shadow" },
|
|
47
|
+
"--dur-fast": { "type": "duration", "meaning": "Fast transition duration" },
|
|
48
|
+
"--dur-normal": { "type": "duration", "meaning": "Standard transition duration" },
|
|
49
|
+
"--ease-standard": { "type": "easing", "meaning": "Standard transition curve" },
|
|
50
|
+
"--z-overlay": { "type": "number", "meaning": "Overlay stacking level" }
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/html.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import postcss from "postcss";
|
|
2
|
+
|
|
3
|
+
// The only non-ADS CSS boundary is document mounting. This is a fixed grammar,
|
|
4
|
+
// not permission for application selectors or additional visual declarations.
|
|
5
|
+
export function isMountCss(text) {
|
|
6
|
+
try {
|
|
7
|
+
const css = postcss.parse(text);
|
|
8
|
+
let declarations = 0;
|
|
9
|
+
for (const rule of css.nodes.filter((node) => node.type !== "comment")) {
|
|
10
|
+
if (rule.type !== "rule") return false;
|
|
11
|
+
const selectors = rule.selector.split(",").map((selector) => selector.trim());
|
|
12
|
+
if (!selectors.length || selectors.some((selector) => !["html", "body", "#root"].includes(selector))) return false;
|
|
13
|
+
for (const decl of rule.nodes.filter((node) => node.type !== "comment")) {
|
|
14
|
+
if (decl.type !== "decl" || decl.important) return false;
|
|
15
|
+
if (!(decl.prop === "height" && decl.value === "100%" || decl.prop === "overflow" && decl.value === "hidden" && selectors.every((selector) => selector === "body"))) return false;
|
|
16
|
+
declarations++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return declarations > 0;
|
|
20
|
+
} catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function inspectHtml(text, report) {
|
|
24
|
+
const clean = text.replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
|
|
25
|
+
for (const match of clean.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)) {
|
|
26
|
+
if (!isMountCss(match[1])) report("html-style", "HTML styles may only set html/body/#root height: 100% and body overflow: hidden. Put visual CSS in ADS owners.", clean.slice(0, match.index).split("\n").length);
|
|
27
|
+
}
|
|
28
|
+
const markup = clean.replace(/<(?:script|style)\b[^>]*>[\s\S]*?<\/(?:script|style)\s*>/gi, (block) => block.replace(/[^\n]/g, " "));
|
|
29
|
+
for (const match of markup.matchAll(/<[a-z][\w:-]*\b[^>]*\sstyle\s*=/gi)) report("html-style", "HTML style attributes are not a visual customization boundary. Use the ADS foundation and components.", markup.slice(0, match.index).split("\n").length);
|
|
30
|
+
}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { initProject } from "./init.mjs";
|
|
2
|
+
export { checkProject } from "./check.mjs";
|
|
3
|
+
export { inspectTokens, tokenContract } from "./tokens.mjs";
|
|
4
|
+
export { generateLintConfig } from "./lint-config.mjs";
|
|
5
|
+
export { generateBrandAssets } from "./brand-assets.mjs";
|
|
6
|
+
// Node-only helpers for registered project lint extensions. They reuse the
|
|
7
|
+
// same validated configuration and property boundary as the core checker.
|
|
8
|
+
export { loadConfig } from "./config.mjs";
|
|
9
|
+
export { importResolver, styleWriteViolation } from "./boundaries.mjs";
|
package/dist/init.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isInside, loadConfig, sourceFiles, supportedFrameworks } from "./config.mjs";
|
|
5
|
+
import { createLintConfig } from "./lint-config.mjs";
|
|
6
|
+
import { foundationFiles } from "./token-policy.mjs";
|
|
7
|
+
import { assertFrameworkCompiler } from "./ui-framework.mjs";
|
|
8
|
+
|
|
9
|
+
const tool = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
10
|
+
const starter = fileURLToPath(new URL("./bootstrap-ds/", import.meta.url));
|
|
11
|
+
const starterPackage = JSON.parse(readFileSync(path.join(starter, "package.json"), "utf8"));
|
|
12
|
+
|
|
13
|
+
function dependencySpec(value) {
|
|
14
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("toolPackage must be a version or local tool tarball path.");
|
|
15
|
+
if (value.startsWith(`${tool.name}@`)) return dependencySpec(value.slice(tool.name.length + 1));
|
|
16
|
+
if (value.startsWith("file:")) {
|
|
17
|
+
const local = value.startsWith("file://") ? fileURLToPath(value) : value.slice(5);
|
|
18
|
+
if (!local) throw new Error("Specify a local tool tarball after file:.");
|
|
19
|
+
return `file:${path.resolve(local)}`;
|
|
20
|
+
}
|
|
21
|
+
if (path.isAbsolute(value) || value.startsWith(".") || value.endsWith(".tgz")) return `file:${path.resolve(value)}`;
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function detectManager(root, pkg, requested) {
|
|
26
|
+
if (requested !== undefined) return requested;
|
|
27
|
+
if (typeof pkg.packageManager === "string") return pkg.packageManager.split("@")[0];
|
|
28
|
+
const bun = ["bun.lock", "bun.lockb"].some((name) => existsSync(path.join(root, name)));
|
|
29
|
+
const npm = existsSync(path.join(root, "package-lock.json"));
|
|
30
|
+
if (bun && npm) throw new Error("Both Bun and npm lockfiles exist. Specify --package-manager bun or npm.");
|
|
31
|
+
if (bun) return "bun";
|
|
32
|
+
if (npm) return "npm";
|
|
33
|
+
if (["pnpm-lock.yaml", "yarn.lock"].some((name) => existsSync(path.join(root, name)))) throw new Error("This version supports npm and Bun projects.");
|
|
34
|
+
return process.env.npm_config_user_agent?.startsWith("bun/") ? "bun" : "npm";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertLocalFile(root, file) {
|
|
38
|
+
if (!isInside(root, file)) throw new Error(`Init writes must stay inside ${root}.`);
|
|
39
|
+
for (let current = file; isInside(root, current); current = path.dirname(current)) {
|
|
40
|
+
const stat = lstatSync(current, { throwIfNoEntry: false });
|
|
41
|
+
if (stat?.isSymbolicLink()) throw new Error(`Init does not write through symlinks: ${current}`);
|
|
42
|
+
if (current === file && stat && !stat.isFile()) throw new Error(`Expected a file: ${file}`);
|
|
43
|
+
if (current !== file && stat && !stat.isDirectory()) throw new Error(`Expected a directory: ${current}`);
|
|
44
|
+
if (current === root) break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function commit(root, updates) {
|
|
49
|
+
const entries = [...updates].map(([file, content]) => {
|
|
50
|
+
assertLocalFile(root, file);
|
|
51
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
52
|
+
return { file, content, before: stat ? readFileSync(file) : null, mode: stat?.mode };
|
|
53
|
+
});
|
|
54
|
+
const staging = mkdtempSync(path.join(root, ".ads-init-"));
|
|
55
|
+
const directories = [];
|
|
56
|
+
const written = [];
|
|
57
|
+
function makeDirectory(directory) {
|
|
58
|
+
if (existsSync(directory)) return;
|
|
59
|
+
makeDirectory(path.dirname(directory));
|
|
60
|
+
mkdirSync(directory);
|
|
61
|
+
directories.push(directory);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
for (const [index, entry] of entries.entries()) {
|
|
65
|
+
const current = existsSync(entry.file) ? readFileSync(entry.file) : null;
|
|
66
|
+
if ((current === null) !== (entry.before === null) || (current && !current.equals(entry.before))) throw new Error(`Project file changed during init: ${entry.file}`);
|
|
67
|
+
const temporary = path.join(staging, String(index));
|
|
68
|
+
writeFileSync(temporary, entry.content);
|
|
69
|
+
if (entry.mode !== undefined) chmodSync(temporary, entry.mode);
|
|
70
|
+
makeDirectory(path.dirname(entry.file));
|
|
71
|
+
renameSync(temporary, entry.file);
|
|
72
|
+
written.push(entry);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
for (const [index, entry] of written.toReversed().entries()) {
|
|
76
|
+
if (entry.before === null) rmSync(entry.file);
|
|
77
|
+
else {
|
|
78
|
+
const restored = path.join(staging, `restore-${index}`);
|
|
79
|
+
writeFileSync(restored, entry.before);
|
|
80
|
+
chmodSync(restored, entry.mode);
|
|
81
|
+
renameSync(restored, entry.file);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const directory of directories.toReversed()) if (!readdirSync(directory).length) rmdirSync(directory);
|
|
85
|
+
throw error;
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(staging, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function initProject(directory = process.cwd(), { toolPackage, packageManager, framework } = {}) {
|
|
92
|
+
const root = path.resolve(directory);
|
|
93
|
+
const packageFile = path.join(root, "package.json");
|
|
94
|
+
assertLocalFile(root, packageFile);
|
|
95
|
+
if (!existsSync(packageFile)) throw new Error("Run ads init inside an existing project containing package.json.");
|
|
96
|
+
const originalPackage = readFileSync(packageFile, "utf8");
|
|
97
|
+
const pkg = JSON.parse(originalPackage);
|
|
98
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) throw new Error("package.json must be an object.");
|
|
99
|
+
for (const field of ["scripts", "dependencies", "devDependencies"]) {
|
|
100
|
+
if (pkg[field] !== undefined && (!pkg[field] || typeof pkg[field] !== "object" || Array.isArray(pkg[field]))) throw new Error(`package.json ${field} must be an object.`);
|
|
101
|
+
}
|
|
102
|
+
for (const [name, script] of Object.entries(pkg.scripts ?? {})) {
|
|
103
|
+
if (typeof script !== "string") throw new Error(`package.json script ${name} must be a string.`);
|
|
104
|
+
}
|
|
105
|
+
const manager = detectManager(root, pkg, packageManager);
|
|
106
|
+
if (!["npm", "bun"].includes(manager)) throw new Error("packageManager must be npm or bun.");
|
|
107
|
+
const run = `${manager} run`;
|
|
108
|
+
const spec = dependencySpec(toolPackage ?? pkg.devDependencies?.[tool.name] ?? pkg.dependencies?.[tool.name] ?? tool.version);
|
|
109
|
+
const { config, dsRoot } = loadConfig(root);
|
|
110
|
+
if (framework !== undefined && !supportedFrameworks.includes(framework)) throw new Error("framework must be react or preact.");
|
|
111
|
+
if (framework !== undefined && config.framework !== null && framework !== config.framework) throw new Error(`ADS is configured for ${config.framework}; --framework ${framework} would change it. Migrate the project and update ds.config.json explicitly before running init.`);
|
|
112
|
+
const selectedFramework = framework ?? config.framework;
|
|
113
|
+
if (selectedFramework === null) throw new Error("Specify --framework react or --framework preact on first init, including when adopting an existing ADS config without a framework.");
|
|
114
|
+
assertFrameworkCompiler(root, selectedFramework);
|
|
115
|
+
config.framework = selectedFramework;
|
|
116
|
+
const lintRoots = [config.sourceDir];
|
|
117
|
+
if (!isInside(path.resolve(root, config.sourceDir), dsRoot)) lintRoots.push(config.dsDir);
|
|
118
|
+
const sourceArguments = lintRoots.map((directory) => `'${directory.replaceAll("'", "'\"'\"'")}'`).join(" ");
|
|
119
|
+
const lint = `ads lint-config . && ads check . && oxlint --deny-warnings --config .ds/oxlintrc.json ${sourceArguments}`;
|
|
120
|
+
const scripts = { ...pkg.scripts };
|
|
121
|
+
if (scripts["lint:ds"] && scripts["lint:ds"] !== lint && !["ads lint-config . && ads check .", "ds lint-config . && ds check ."].some(prefix => scripts["lint:ds"].startsWith(prefix))) throw new Error("An existing custom lint:ds script would conflict. Rename it or integrate ADS checks before running init.");
|
|
122
|
+
scripts["lint:ds"] = lint;
|
|
123
|
+
if (scripts["assets:ds"] && !["ads assets", "ds assets"].includes(scripts["assets:ds"])) throw new Error("An existing custom assets:ds script would conflict. Rename it or integrate ADS asset generation before running init.");
|
|
124
|
+
scripts["assets:ds"] = "ads assets";
|
|
125
|
+
if (!scripts["dev:showcase"] || scripts["dev:showcase"] === "ds showcase") scripts["dev:showcase"] = "ads showcase";
|
|
126
|
+
scripts["ads-mcp"] ??= "ads mcp --mutations";
|
|
127
|
+
scripts["ads-read-mcp"] ??= "ads mcp";
|
|
128
|
+
const managedBuild = [`${run} lint:ds`, `${run} lint:ds && ${run} assets:ds`].includes(scripts.build);
|
|
129
|
+
if (scripts.build && !managedBuild) {
|
|
130
|
+
const previous = scripts.prebuild ?? "";
|
|
131
|
+
const remaining = previous.replace(/^(?:npm|bun) run lint:ds(?:\s*&&\s*(?:npm|bun) run assets:ds)?(?:\s*&&\s*|$)/, "");
|
|
132
|
+
scripts.prebuild = `${run} lint:ds && ${run} assets:ds${remaining ? ` && ${remaining}` : ""}`;
|
|
133
|
+
} else scripts.build = `${run} lint:ds && ${run} assets:ds`;
|
|
134
|
+
if (scripts["build:showcase"]) {
|
|
135
|
+
const previous = scripts["prebuild:showcase"] ?? "";
|
|
136
|
+
const remaining = previous.replace(/^(?:npm|bun) run lint:ds(?:\s*&&\s*(?:npm|bun) run assets:ds)?(?:\s*&&\s*|$)/, "");
|
|
137
|
+
scripts["prebuild:showcase"] = `${run} lint:ds && ${run} assets:ds${remaining ? ` && ${remaining}` : ""}`;
|
|
138
|
+
}
|
|
139
|
+
pkg.scripts = scripts;
|
|
140
|
+
pkg.devDependencies = { ...pkg.devDependencies, [tool.name]: spec };
|
|
141
|
+
if (!pkg.devDependencies.oxlint && !pkg.dependencies?.oxlint) pkg.devDependencies.oxlint = starterPackage.devDependencies.oxlint;
|
|
142
|
+
if (pkg.dependencies?.[tool.name]) delete pkg.dependencies[tool.name];
|
|
143
|
+
|
|
144
|
+
const updates = new Map();
|
|
145
|
+
function addMissing(file, content) { assertLocalFile(root, file); if (!existsSync(file)) updates.set(file, content); }
|
|
146
|
+
const tokenRoot = path.join(dsRoot, config.tokenDir);
|
|
147
|
+
const existingTokens = sourceFiles(tokenRoot).filter((file) => file.endsWith(".css"));
|
|
148
|
+
const tokenFiles = existingTokens.length ? existingTokens : foundationFiles.filter((name) => existsSync(path.join(starter, "design-system/tokens", name))).map((name) => {
|
|
149
|
+
const file = path.join(tokenRoot, name);
|
|
150
|
+
addMissing(file, readFileSync(path.join(starter, "design-system/tokens", name)));
|
|
151
|
+
return file;
|
|
152
|
+
});
|
|
153
|
+
if (!existingTokens.length) {
|
|
154
|
+
for (const name of ["Foundation.scenario.tsx", "Foundation.fixture.ts"]) {
|
|
155
|
+
let content = readFileSync(path.join(starter, "design-system/tokens", name), "utf8");
|
|
156
|
+
if (selectedFramework === "preact" && name === "Foundation.scenario.tsx") content = content.replace('from "react"', 'from "preact/hooks"');
|
|
157
|
+
addMissing(path.join(tokenRoot, name), content);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
tokenFiles.sort((a, b) => foundationFiles.indexOf(path.basename(a)) - foundationFiles.indexOf(path.basename(b)) || a.localeCompare(b));
|
|
161
|
+
const stylesFile = path.join(dsRoot, config.stylesEntry);
|
|
162
|
+
const relativeImport = (from, to) => {
|
|
163
|
+
const relative = path.relative(path.dirname(from), to).split(path.sep).join("/");
|
|
164
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
165
|
+
};
|
|
166
|
+
addMissing(stylesFile, tokenFiles.map((file) => `@import ${JSON.stringify(relativeImport(stylesFile, file))};`).join("\n") + "\n");
|
|
167
|
+
const entryFile = path.join(dsRoot, config.publicEntry);
|
|
168
|
+
addMissing(entryFile, `// Export your project's public components and types here.\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\nexport {};\n`);
|
|
169
|
+
for (const section of ["components", "patterns", "composition"]) addMissing(path.join(dsRoot, section, ".gitkeep"), "");
|
|
170
|
+
addMissing(path.join(dsRoot, "assets/brand/README.md"), readFileSync(path.join(starter, "design-system/assets/brand/README.md")));
|
|
171
|
+
addMissing(path.join(dsRoot, "assets/fonts/.gitkeep"), "");
|
|
172
|
+
const configFile = path.join(root, "ds.config.json");
|
|
173
|
+
assertLocalFile(root, configFile);
|
|
174
|
+
if (!existsSync(configFile)) updates.set(configFile, `${JSON.stringify(config, null, 2)}\n`);
|
|
175
|
+
else {
|
|
176
|
+
const original = readFileSync(configFile, "utf8");
|
|
177
|
+
const saved = JSON.parse(original);
|
|
178
|
+
if (saved.framework === undefined) {
|
|
179
|
+
const indent = original.match(/\n([\t ]+)"/)?.[1] ?? " ";
|
|
180
|
+
updates.set(configFile, `${JSON.stringify({ ...saved, framework: selectedFramework }, null, indent)}\n`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const lintConfigFile = path.join(root, ".ds/oxlintrc.json");
|
|
184
|
+
updates.set(lintConfigFile, `${JSON.stringify(createLintConfig({ root, config }), null, 2)}\n`);
|
|
185
|
+
const ignoreFile = path.join(root, ".gitignore");
|
|
186
|
+
assertLocalFile(root, ignoreFile);
|
|
187
|
+
const ignore = existsSync(ignoreFile) ? readFileSync(ignoreFile, "utf8") : "";
|
|
188
|
+
if (!ignore.split(/\r?\n/).some((line) => [".ds/", ".ds", "/.ds/", "/.ds"].includes(line))) updates.set(ignoreFile, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}.ds/\n`);
|
|
189
|
+
const indent = originalPackage.match(/\n([\t ]+)"/)?.[1] ?? " ";
|
|
190
|
+
const nextPackage = `${JSON.stringify(pkg, null, indent)}\n`;
|
|
191
|
+
if (nextPackage !== originalPackage) updates.set(packageFile, nextPackage);
|
|
192
|
+
commit(root, updates);
|
|
193
|
+
return { directory: root, framework: selectedFramework, packageManager: manager, lintConfig: lintConfigFile, toolDependency: { name: tool.name, spec }, updatedFiles: [...updates.keys()] };
|
|
194
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { loadConfig } from "./config.mjs";
|
|
5
|
+
import { configurationFiles } from "./units.mjs";
|
|
6
|
+
|
|
7
|
+
export function createLintConfig({ root, config }) {
|
|
8
|
+
const names = new Set(["ads"]);
|
|
9
|
+
const projectPlugins = config.oxlint.plugins.map((plugin) => {
|
|
10
|
+
if (names.has(plugin.name)) throw new Error(`Duplicate or reserved Oxlint plugin name: ${plugin.name}`);
|
|
11
|
+
names.add(plugin.name);
|
|
12
|
+
const specifier = path.resolve(root, plugin.path);
|
|
13
|
+
if (!existsSync(specifier)) throw new Error(`Project plugin not found: ${plugin.path}`);
|
|
14
|
+
return { name: plugin.name, specifier };
|
|
15
|
+
});
|
|
16
|
+
const options = { projectRoot: root, sourceDir: config.sourceDir, dsDir: config.dsDir, publicAlias: config.publicAlias, stylesEntry: config.stylesEntry, inlineStyleFiles: config.inlineStyleFiles,
|
|
17
|
+
configurationFiles: [...configurationFiles({ root, config, sourceRoot: path.resolve(root,config.sourceDir), dsRoot: path.resolve(root,config.dsDir) })].sort() };
|
|
18
|
+
return {
|
|
19
|
+
jsPlugins: [{ name: "ads", specifier: fileURLToPath(new URL("./oxlint.mjs", import.meta.url)) }, ...projectPlugins],
|
|
20
|
+
rules: {
|
|
21
|
+
"ads/public-import": ["error", options],
|
|
22
|
+
"ads/no-inline-style": ["error", options],
|
|
23
|
+
"ads/no-app-import": ["error", options],
|
|
24
|
+
"ads/showcase-import": ["error", options],
|
|
25
|
+
...config.oxlint.rules,
|
|
26
|
+
},
|
|
27
|
+
// The generated config lives in .ds/. Resolve project-relative file globs
|
|
28
|
+
// here so the same scoped policy survives a relocated checkout.
|
|
29
|
+
overrides: config.oxlint.overrides.map((override) => ({ ...override, files: override.files.map((file) => path.resolve(root, file).replaceAll("\\", "/")) })),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function generateLintConfig(directory) {
|
|
34
|
+
const project = loadConfig(directory);
|
|
35
|
+
const generated = createLintConfig(project);
|
|
36
|
+
const file = path.join(project.root, ".ds", "oxlintrc.json");
|
|
37
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
38
|
+
writeFileSync(file, `${JSON.stringify(generated, null, 2)}\n`);
|
|
39
|
+
return { file };
|
|
40
|
+
}
|