@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/oxlint.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defaultConfig, importViolation } from "./config.mjs";
|
|
3
|
+
import { appImportViolation, showcaseImportViolation, importResolver } from "./boundaries.mjs";
|
|
4
|
+
|
|
5
|
+
const schema = [{
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
projectRoot: { type: "string" },
|
|
9
|
+
dsDir: { type: "string" },
|
|
10
|
+
sourceDir: { type: "string" },
|
|
11
|
+
publicAlias: { type: "string" },
|
|
12
|
+
stylesEntry: { type: "string" },
|
|
13
|
+
inlineStyleFiles: { type: "array", items: { type: "string" } },
|
|
14
|
+
configurationFiles: { type: "array", items: { type: "string" } },
|
|
15
|
+
},
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
}];
|
|
18
|
+
|
|
19
|
+
function options(context) {
|
|
20
|
+
const user = context.options[0] ?? {};
|
|
21
|
+
const root = path.resolve(user.projectRoot ?? process.cwd());
|
|
22
|
+
const config = { ...defaultConfig, ...user };
|
|
23
|
+
return { root, config, dsRoot: path.resolve(root, config.dsDir) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
meta: { name: "ads" },
|
|
28
|
+
rules: {
|
|
29
|
+
"showcase-import": {
|
|
30
|
+
meta: { type: "problem", schema, messages: { showcase: "Showcase code and styles are development fixtures; runtime code must not import them." } },
|
|
31
|
+
create(context) {
|
|
32
|
+
const settings = options(context);
|
|
33
|
+
const resolve = importResolver(settings);
|
|
34
|
+
const filename = path.resolve(context.getFilename());
|
|
35
|
+
function inspect(node) {
|
|
36
|
+
if (typeof node.source?.value === "string" && showcaseImportViolation(node.source.value, filename, settings, resolve)) context.report({ node: node.source, messageId: "showcase" });
|
|
37
|
+
}
|
|
38
|
+
return { ImportDeclaration: inspect, ExportNamedDeclaration: inspect, ExportAllDeclaration: inspect, ImportExpression: inspect };
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"no-app-import": {
|
|
42
|
+
meta: { type: "problem", schema, messages: { app: "ADS implementations and showcase fixtures must not import app code. Use public ADS APIs and local fixture data." } },
|
|
43
|
+
create(context) {
|
|
44
|
+
const settings = options(context);
|
|
45
|
+
const resolve = importResolver(settings);
|
|
46
|
+
const filename = path.resolve(context.getFilename());
|
|
47
|
+
function inspect(node) {
|
|
48
|
+
if (typeof node.source?.value === "string" && appImportViolation(node.source.value, filename, settings, resolve)) context.report({ node: node.source, messageId: "app" });
|
|
49
|
+
}
|
|
50
|
+
return { ImportDeclaration: inspect, ExportNamedDeclaration: inspect, ExportAllDeclaration: inspect, ImportExpression: inspect };
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"public-import": {
|
|
54
|
+
meta: { type: "problem", schema, messages: { public: "Import through the project's public ADS alias, not implementation paths." } },
|
|
55
|
+
create(context) {
|
|
56
|
+
const settings = options(context);
|
|
57
|
+
const resolve = importResolver(settings);
|
|
58
|
+
const filename = path.resolve(context.getFilename());
|
|
59
|
+
const buildFiles = new Set((settings.config.configurationFiles ?? []).map(file => path.resolve(settings.root,file)));
|
|
60
|
+
function inspect(node) {
|
|
61
|
+
if (typeof node.source?.value !== "string") return;
|
|
62
|
+
if (buildFiles.has(filename) && resolve(node.source.value,filename).every(file=>buildFiles.has(file))) return;
|
|
63
|
+
if (importViolation(node.source.value, filename, settings, resolve)) context.report({ node: node.source, messageId: "public" });
|
|
64
|
+
}
|
|
65
|
+
return { ImportDeclaration: inspect, ExportNamedDeclaration: inspect, ExportAllDeclaration: inspect, ImportExpression: inspect };
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"no-inline-style": {
|
|
69
|
+
meta: { type: "problem", schema, messages: { inline: "Move visual policy into the local ADS, or document a narrow exception in inlineStyleFiles." } },
|
|
70
|
+
create(context) {
|
|
71
|
+
const { root, config } = options(context);
|
|
72
|
+
const allowed = config.inlineStyleFiles.some((file) => path.resolve(root, file) === path.resolve(context.getFilename()));
|
|
73
|
+
return {
|
|
74
|
+
JSXAttribute(node) {
|
|
75
|
+
if (!allowed && node.name.type === "JSXIdentifier" && node.name.name === "style") context.report({ node, messageId: "inline" });
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import valueParser from "postcss-value-parser";
|
|
6
|
+
import { isInside } from "./config.mjs";
|
|
7
|
+
|
|
8
|
+
export const PREVIEW_APP_URI = "ui://ads/preview";
|
|
9
|
+
export const PREVIEW_MANIFEST_URI = "ads://preview/manifest";
|
|
10
|
+
export const assetUriPattern = /^ads:\/\/assets\/[a-f0-9]{64}\.[a-z0-9]+$/;
|
|
11
|
+
const hash = value => createHash("sha256").update(value).digest("hex");
|
|
12
|
+
const unknownResource = message => Object.assign(new Error(message), { code: "ADS_PREVIEW_NOT_FOUND" });
|
|
13
|
+
const mime = file => ({ js: "text/javascript", css: "text/css", json: "application/json", svg: "image/svg+xml", woff2: "font/woff2", woff: "font/woff", ttf: "font/ttf", otf: "font/otf", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif", wasm: "application/wasm" })[file.split(".").pop()] ?? "application/octet-stream";
|
|
14
|
+
function atomic(file, bytes) {
|
|
15
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
16
|
+
if (realpathSync(path.dirname(file)) !== path.dirname(file)) throw new Error("Preview output cannot resolve through a symlink.");
|
|
17
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
18
|
+
try { writeFileSync(temporary, bytes); renameSync(temporary, file); }
|
|
19
|
+
finally { rmSync(temporary, { force: true }); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Registry entries remain project-owned; these virtual entries normalize mount/unmount. */
|
|
23
|
+
export function previewEntriesPlugin(entries, mountEntry) {
|
|
24
|
+
const ids = new Map(entries.map((entry, index) => [`ads-preview-entry:${index}`, entry]));
|
|
25
|
+
return { name: "ads-preview-resource-entries",
|
|
26
|
+
resolveId(id) { if (ids.has(id)) return `\0${id}`; },
|
|
27
|
+
load(id) {
|
|
28
|
+
const entry = ids.get(id.slice(1));
|
|
29
|
+
if (!entry) return;
|
|
30
|
+
return `import { mountScenario } from ${JSON.stringify(mountEntry)};import Scenario from ${JSON.stringify(path.resolve(entry.root, entry.file))};export const mount = container => mountScenario(Scenario,container);`;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Convert Rollup output to registered MCP resources without evaluating project code. */
|
|
36
|
+
export function createPreviewResources(output, options, entries) {
|
|
37
|
+
const bundle = (Array.isArray(output) ? output : [output]).flatMap(result => result.output);
|
|
38
|
+
const original = item => Buffer.from(item.type === "chunk" ? item.code : item.source);
|
|
39
|
+
// Rollup's emitted import names include dependency hashes. Include original
|
|
40
|
+
// content too; an immutable URI must never represent two different outputs.
|
|
41
|
+
// Bump the transform version whenever emitted resource rewriting changes.
|
|
42
|
+
const uris = new Map(bundle.map(item => [item.fileName, `ads://assets/${hash(Buffer.concat([Buffer.from("ads-preview-v1\n" + item.fileName + "\n"), original(item)]))}.${item.fileName.split(".").pop()}`]));
|
|
43
|
+
const records = {};
|
|
44
|
+
const contents = new Map();
|
|
45
|
+
function reference(from, value) {
|
|
46
|
+
if (/^(?:[a-z]+:|#|\/\/)/i.test(value)) return null;
|
|
47
|
+
const file = value.startsWith("/") ? value.slice(1) : path.posix.normalize(path.posix.join(path.posix.dirname(from), value));
|
|
48
|
+
return uris.get(file) ?? null;
|
|
49
|
+
}
|
|
50
|
+
for (const item of bundle) {
|
|
51
|
+
const uri = uris.get(item.fileName);
|
|
52
|
+
const references = new Set();
|
|
53
|
+
let bytes = original(item);
|
|
54
|
+
if (item.type === "chunk") {
|
|
55
|
+
const source = ts.createSourceFile(item.fileName, item.code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
56
|
+
const replacements = [];
|
|
57
|
+
function visit(node) {
|
|
58
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "Worker" && node.arguments?.[0]) {
|
|
59
|
+
let bundledWorker = false;
|
|
60
|
+
function workerReference(child) {
|
|
61
|
+
if (ts.isStringLiteralLike(child)) {
|
|
62
|
+
const uri = reference(item.fileName, child.text);
|
|
63
|
+
const asset = bundle.find(candidate => uris.get(candidate.fileName) === uri);
|
|
64
|
+
if (asset?.type === "asset" && mime(asset.fileName) === "text/javascript") {
|
|
65
|
+
const worker = ts.createSourceFile(asset.fileName, original(asset).toString(), ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
66
|
+
if (worker.statements.some(statement => ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement) || statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword))) throw new Error("Preview workers must be self-contained IIFE output. Set Vite worker.format to iife; keep the production worker build unchanged.");
|
|
67
|
+
bundledWorker = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
ts.forEachChild(child, workerReference);
|
|
71
|
+
}
|
|
72
|
+
workerReference(node.arguments[0]);
|
|
73
|
+
// Module worker fetches reject opaque sandbox origins. Vite's IIFE
|
|
74
|
+
// output is already a classic script, so adapt only its preview call.
|
|
75
|
+
const settings = node.arguments[1];
|
|
76
|
+
if (bundledWorker && settings) {
|
|
77
|
+
replacements.push([settings.getStart(source), settings.getStart(source), "{...("]);
|
|
78
|
+
replacements.push([settings.end, settings.end, '),type:"classic"}']);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (ts.isStringLiteralLike(node)) {
|
|
82
|
+
const target = reference(item.fileName, node.text);
|
|
83
|
+
if (target) {
|
|
84
|
+
references.add(target);
|
|
85
|
+
if (ts.isCallExpression(node.parent) && node.parent.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
86
|
+
replacements.push([node.parent.getStart(source), node.parent.end, `globalThis.__dsPreviewImport(${JSON.stringify(target)})`]);
|
|
87
|
+
} else replacements.push([node.getStart(source), node.end, JSON.stringify(target)]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
ts.forEachChild(node, visit);
|
|
91
|
+
}
|
|
92
|
+
visit(source);
|
|
93
|
+
let code = item.code;
|
|
94
|
+
for (const [start, end, replacement] of replacements.sort((a, b) => b[0] - a[0])) code = code.slice(0, start) + replacement + code.slice(end);
|
|
95
|
+
bytes = Buffer.from(code);
|
|
96
|
+
} else if (item.fileName.endsWith(".css")) {
|
|
97
|
+
const parsed = valueParser(bytes.toString());
|
|
98
|
+
parsed.walk(node => {
|
|
99
|
+
if (node.type !== "function" || node.value !== "url") return;
|
|
100
|
+
const value = valueParser.stringify(node.nodes).replace(/^["']|["']$/g, "");
|
|
101
|
+
const target = reference(item.fileName, value);
|
|
102
|
+
if (target) { references.add(target); node.nodes = [{ type: "string", quote: '"', value: target }]; }
|
|
103
|
+
});
|
|
104
|
+
bytes = Buffer.from(parsed.toString());
|
|
105
|
+
}
|
|
106
|
+
const imports = item.type === "chunk" ? item.imports.map(file => uris.get(file)).filter(Boolean) : [];
|
|
107
|
+
const dynamicImports = item.type === "chunk" ? item.dynamicImports.map(file => uris.get(file)).filter(Boolean) : [];
|
|
108
|
+
for (const dependency of [...imports, ...dynamicImports]) references.delete(dependency);
|
|
109
|
+
const styles = item.type === "chunk" ? [...item.viteMetadata?.importedCss ?? []].map(file => uris.get(file)).filter(Boolean) : [];
|
|
110
|
+
records[uri] = { mimeType: mime(item.fileName), size: bytes.length, digest: `sha256:${hash(bytes)}`, kind: item.type === "chunk" ? "module" : "asset", imports, dynamicImports, styles, references: [...references] };
|
|
111
|
+
contents.set(uri, bytes);
|
|
112
|
+
}
|
|
113
|
+
const scenarios = entries.map((entry, index) => {
|
|
114
|
+
const chunk = bundle.find(item => item.type === "chunk" && item.facadeModuleId === `\0ads-preview-entry:${index}`);
|
|
115
|
+
if (!chunk) throw new Error(`Missing compiled scenario entry: ${entry.id}`);
|
|
116
|
+
return { id: entry.id, title: entry.title, kind: entry.kind, module: uris.get(chunk.fileName) };
|
|
117
|
+
});
|
|
118
|
+
const graph = { schemaVersion: 1, framework: options.config.framework, assets: records, scenarios };
|
|
119
|
+
const revision = hash(JSON.stringify(graph));
|
|
120
|
+
validateManifest({ ...graph, revision });
|
|
121
|
+
const target = path.resolve(options.root, options.config.mcp.previewManifest ?? ".ds/preview/manifest.json");
|
|
122
|
+
const assetsRoot = path.join(path.dirname(target), "assets");
|
|
123
|
+
const assetFiles = [...contents.keys()].map(uri => path.join(assetsRoot, uri.slice("ads://assets/".length)));
|
|
124
|
+
return { assetFiles, publish(session) {
|
|
125
|
+
const encoded = JSON.stringify({ ...graph, revision, session });
|
|
126
|
+
if (Buffer.byteLength(encoded) > 2 * 1024 * 1024) throw new Error("Preview manifest exceeds the 2 MiB resource limit.");
|
|
127
|
+
const history = path.join(path.dirname(target), "history");
|
|
128
|
+
if (existsSync(target)) {
|
|
129
|
+
try {
|
|
130
|
+
const previous = readPreviewManifest(options);
|
|
131
|
+
if (previous && previous.revision !== revision) atomic(path.join(history, `${previous.revision}.json`), JSON.stringify(previous));
|
|
132
|
+
} catch { /* A successful rebuild replaces corrupt generated metadata. */ }
|
|
133
|
+
}
|
|
134
|
+
for (const [uri, bytes] of contents) atomic(path.join(assetsRoot, uri.slice("ads://assets/".length)), bytes);
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
const descriptor = JSON.stringify({ schemaVersion: 1, manifestUri: PREVIEW_MANIFEST_URI, revision, id: entry.id });
|
|
137
|
+
atomic(path.join(options.root, entry.artifact), `<!doctype html><html><head></head><body><script type="application/json" id="ads-preview-resource">${descriptor.replaceAll("<", "\\u003c")}</script></body></html>`);
|
|
138
|
+
}
|
|
139
|
+
atomic(target, encoded);
|
|
140
|
+
const retained = existsSync(history) ? readdirSync(history).filter(name => /^[a-f0-9]{64}\.json$/.test(name)).sort((a, b) => statSync(path.join(history, b)).mtimeMs - statSync(path.join(history, a)).mtimeMs) : [];
|
|
141
|
+
const keep = new Set(contents.keys());
|
|
142
|
+
for (const file of retained.slice(0, 6)) {
|
|
143
|
+
try { for (const uri of Object.keys(readManifestFile(path.join(history, file)).assets)) keep.add(uri); }
|
|
144
|
+
catch { rmSync(path.join(history, file), { force: true }); }
|
|
145
|
+
}
|
|
146
|
+
for (const file of retained.slice(6)) rmSync(path.join(history, file));
|
|
147
|
+
for (const file of readdirSync(assetsRoot)) if (/^[a-f0-9]{64}\.[a-z0-9]+$/.test(file) && !keep.has(`ads://assets/${file}`)) rmSync(path.join(assetsRoot, file));
|
|
148
|
+
} };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function readPreviewManifest(options, { verify = false } = {}) {
|
|
152
|
+
const relative = options.config.mcp.previewManifest;
|
|
153
|
+
if (!relative) return null;
|
|
154
|
+
const file = path.resolve(options.root, relative);
|
|
155
|
+
if (!existsSync(file)) return null;
|
|
156
|
+
const manifest = readManifestFile(file);
|
|
157
|
+
if (verify) for (const uri of Object.keys(manifest.assets)) readPreviewAsset(options, uri, manifest);
|
|
158
|
+
return manifest;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readManifestFile(file) {
|
|
162
|
+
if (realpathSync(file) !== file || !statSync(file).isFile() || statSync(file).size > 2 * 1024 * 1024) throw new Error("Invalid preview manifest file.");
|
|
163
|
+
return validateManifest(JSON.parse(readFileSync(file, "utf8")));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateManifest(manifest) {
|
|
167
|
+
if (manifest.schemaVersion !== 1 || !["react", "preact"].includes(manifest.framework) || typeof manifest.revision !== "string" || !manifest.assets || typeof manifest.assets !== "object" || Array.isArray(manifest.assets) || !Array.isArray(manifest.scenarios)) throw new Error("Invalid preview resource manifest. Rebuild the project previews.");
|
|
168
|
+
for (const [uri, record] of Object.entries(manifest.assets)) {
|
|
169
|
+
if (!record || !assetUriPattern.test(uri) || !/^sha256:[a-f0-9]{64}$/.test(record.digest) || !["module", "asset"].includes(record.kind)
|
|
170
|
+
|| !Number.isSafeInteger(record.size) || record.size < 0 || record.size > 16 * 1024 * 1024 || typeof record.mimeType !== "string"
|
|
171
|
+
|| !["imports", "dynamicImports", "styles", "references"].every(key => Array.isArray(record[key]) && record[key].every(ref => Object.hasOwn(manifest.assets, ref)))) throw new Error("Invalid preview asset dependency graph.");
|
|
172
|
+
}
|
|
173
|
+
if (manifest.scenarios.some(entry => !entry || typeof entry.id !== "string" || manifest.assets[entry.module]?.kind !== "module") || new Set(manifest.scenarios.map(entry => entry.id)).size !== manifest.scenarios.length) throw new Error("Missing or duplicate scenario module in preview manifest.");
|
|
174
|
+
const { revision, session: _session, ...graph } = manifest;
|
|
175
|
+
if (hash(JSON.stringify(graph)) !== revision) throw new Error("Preview manifest integrity check failed. Rebuild previews.");
|
|
176
|
+
return manifest;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function readPreviewAsset(options, uri, manifest = readPreviewManifest(options)) {
|
|
180
|
+
if (!assetUriPattern.test(uri)) throw unknownResource("Unknown preview asset resource.");
|
|
181
|
+
let record = manifest?.assets?.[uri];
|
|
182
|
+
if (!record) {
|
|
183
|
+
const history = path.join(path.dirname(path.resolve(options.root, options.config.mcp.previewManifest)), "history");
|
|
184
|
+
for (const file of existsSync(history) ? readdirSync(history).filter(name => /^[a-f0-9]{64}\.json$/.test(name)).slice(0, 6) : []) {
|
|
185
|
+
const candidate = path.join(history, file);
|
|
186
|
+
try { record = readManifestFile(candidate).assets[uri]; }
|
|
187
|
+
catch { continue; }
|
|
188
|
+
if (record) break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!record) throw unknownResource("Unknown or expired preview asset resource. Refresh the manifest.");
|
|
192
|
+
const directory = path.join(path.dirname(path.resolve(options.root, options.config.mcp.previewManifest)), "assets");
|
|
193
|
+
const file = path.join(directory, uri.slice("ads://assets/".length));
|
|
194
|
+
if (!isInside(directory, file) || realpathSync(file) !== file || statSync(file).size > 16 * 1024 * 1024) throw new Error("Invalid preview asset file.");
|
|
195
|
+
const bytes = readFileSync(file);
|
|
196
|
+
if (bytes.length !== record.size || `sha256:${hash(bytes)}` !== record.digest) throw new Error("Preview resource does not match its successful build. Rebuild before reviewing.");
|
|
197
|
+
return { uri, mimeType: record.mimeType, ...(/^(?:text\/|application\/json|image\/svg)/.test(record.mimeType) ? { text: bytes.toString() } : { blob: bytes.toString("base64") }) };
|
|
198
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const rootNames = ["README.md", "ADS.md", "DS.md", "ARCHITECTURE.md", "Architecture.md", "GUIDE.md", "MCP.md", "CHANGELOG.md"];
|
|
5
|
+
|
|
6
|
+
/** Recognize documentation ownership even before a new guide exists. */
|
|
7
|
+
export function isProjectDocument(file) {
|
|
8
|
+
return rootNames.includes(file) || /^(?:docs|doc)\/.+\.md$/i.test(file) && !file.split("/").some(part => part.startsWith("."));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Shared guide discovery for MCP resources, workflow context and editable units. */
|
|
12
|
+
export function projectGuideFiles(root) {
|
|
13
|
+
const files = rootNames.filter(file => existsSync(path.join(root, file)) && lstatSync(path.join(root, file)).isFile());
|
|
14
|
+
function walk(relative) {
|
|
15
|
+
const directory = path.join(root, relative);
|
|
16
|
+
if (!existsSync(directory) || !lstatSync(directory).isDirectory()) return;
|
|
17
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
18
|
+
if (entry.name.startsWith(".")) continue;
|
|
19
|
+
const file = `${relative}/${entry.name}`;
|
|
20
|
+
if (entry.isDirectory()) walk(file);
|
|
21
|
+
else if (entry.isFile() && isProjectDocument(file)) files.push(file);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
walk("docs");
|
|
25
|
+
walk("doc");
|
|
26
|
+
return files.sort();
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { importedUiName } from "./ui-framework.mjs";
|
|
3
|
+
|
|
4
|
+
const visualName = /^(?:background(?:Color)?|color|fill|stroke|(?:\w+)?ColorToken)$/i;
|
|
5
|
+
|
|
6
|
+
function propertyName(node) {
|
|
7
|
+
return node && (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) ? node.text : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stringKeys(node, checker, seen = new Set()) {
|
|
11
|
+
if (!node || seen.has(node)) return [];
|
|
12
|
+
seen.add(node);
|
|
13
|
+
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) return [node.literal.text];
|
|
14
|
+
if (ts.isUnionTypeNode(node)) return node.types.flatMap((type) => stringKeys(type, checker, seen));
|
|
15
|
+
if (ts.isTypeReferenceNode(node)) return (checker.getSymbolAtLocation(node.typeName)?.declarations ?? []).flatMap((decl) => ts.isTypeAliasDeclaration(decl) ? stringKeys(decl.type, checker, seen) : []);
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Inspect locally declared exported *Props APIs without loading React at runtime.
|
|
20
|
+
// Follow local aliases and inheritance, and distinguish Omit/Pick's actual keys.
|
|
21
|
+
export function inspectPublicProps(source, checker, report) {
|
|
22
|
+
function inspect(node, excluded = new Set(), seen = new Set()) {
|
|
23
|
+
if (!node || seen.has(node)) return;
|
|
24
|
+
seen = new Set([...seen, node]);
|
|
25
|
+
if (ts.isPropertySignature(node)) {
|
|
26
|
+
const name = propertyName(node.name);
|
|
27
|
+
if (excluded.has(name)) return;
|
|
28
|
+
if (name === "style") report(node, "public-style-prop", "Public ADS props must omit style. Use semantic props and documented token hooks.");
|
|
29
|
+
if (visualName.test(name ?? "") && arbitraryString(node.type, new Set())) report(node, "visual-prop", "Public visual props must use semantic variants or a finite token union, not arbitrary strings.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (ts.isTypeReferenceNode(node) || ts.isExpressionWithTypeArguments(node)) {
|
|
33
|
+
const name = ts.isTypeReferenceNode(node) ? node.typeName : node.expression;
|
|
34
|
+
const text = ts.isIdentifier(name) ? name.text : null;
|
|
35
|
+
const args = node.typeArguments ?? [];
|
|
36
|
+
if (text === "Omit" && args.length === 2) {
|
|
37
|
+
inspect(args[0], new Set([...excluded, ...stringKeys(args[1], checker)]), seen);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (text === "Pick" && args.length === 2) {
|
|
41
|
+
if (stringKeys(args[1], checker).includes("style") && !excluded.has("style")) inspect(args[0], excluded, seen);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const react = importedUiName(name, checker)?.replace(/^JSX\./, "");
|
|
45
|
+
if (react && /^(?:\w*(?:HTML|SVG)Attributes|DOMAttributes|ComponentProps(?:WithRef|WithoutRef)?)$/.test(react)) {
|
|
46
|
+
if (!excluded.has("style")) report(node, "public-style-prop", 'Public ADS props inherited from React/Preact DOM attributes must omit "style".');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const decl of checker.getSymbolAtLocation(name)?.declarations ?? []) {
|
|
50
|
+
if (ts.isTypeAliasDeclaration(decl)) inspect(decl.type, excluded, seen);
|
|
51
|
+
if (ts.isInterfaceDeclaration(decl)) inspect(decl, excluded, seen);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
ts.forEachChild(node, (child) => inspect(child, excluded, seen));
|
|
56
|
+
}
|
|
57
|
+
function arbitraryString(node, seen) {
|
|
58
|
+
if (!node || seen.has(node)) return false;
|
|
59
|
+
seen.add(node);
|
|
60
|
+
if ([ts.SyntaxKind.StringKeyword, ts.SyntaxKind.AnyKeyword, ts.SyntaxKind.UnknownKeyword].includes(node.kind)) return true;
|
|
61
|
+
if (ts.isTemplateLiteralTypeNode(node)) return node.templateSpans.some((span) => arbitraryString(span.type, seen));
|
|
62
|
+
if (ts.isUnionTypeNode(node)) return node.types.some((type) => arbitraryString(type, seen));
|
|
63
|
+
if (ts.isTypeReferenceNode(node)) return (checker.getSymbolAtLocation(node.typeName)?.declarations ?? []).some((decl) => ts.isTypeAliasDeclaration(decl) && arbitraryString(decl.type, seen));
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const symbol = checker.getSymbolAtLocation(source);
|
|
67
|
+
for (const exported of symbol ? checker.getExportsOfModule(symbol) : []) {
|
|
68
|
+
if (!exported.name.endsWith("Props")) continue;
|
|
69
|
+
for (const decl of exported.declarations ?? []) {
|
|
70
|
+
if (ts.isTypeAliasDeclaration(decl)) inspect(decl.type);
|
|
71
|
+
if (ts.isInterfaceDeclaration(decl)) inspect(decl);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|