mdx-artifacts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/README.zh-CN.md +129 -0
  4. package/agents/AGENTS.snippet.md +13 -0
  5. package/artifact-docs/examples/commentable-feedback.mdx +126 -0
  6. package/artifact-docs/examples/decision-matrix.mdx +80 -0
  7. package/artifact-docs/examples/layout-composition.mdx +216 -0
  8. package/artifact-docs/examples/streamlit-style-mixed.mdx +183 -0
  9. package/dist/lib/cli/artifact-state.d.ts +27 -0
  10. package/dist/lib/cli/artifact-state.js +115 -0
  11. package/dist/lib/cli/build.d.ts +1 -0
  12. package/dist/lib/cli/build.js +25 -0
  13. package/dist/lib/cli/components.d.ts +3 -0
  14. package/dist/lib/cli/components.js +58 -0
  15. package/dist/lib/cli/config.d.ts +2 -0
  16. package/dist/lib/cli/config.js +36 -0
  17. package/dist/lib/cli/dev.d.ts +1 -0
  18. package/dist/lib/cli/dev.js +24 -0
  19. package/dist/lib/cli/index.d.ts +2 -0
  20. package/dist/lib/cli/index.js +69 -0
  21. package/dist/lib/cli/review.d.ts +33 -0
  22. package/dist/lib/cli/review.js +390 -0
  23. package/dist/lib/cli/scaffold.d.ts +1 -0
  24. package/dist/lib/cli/scaffold.js +56 -0
  25. package/dist/lib/cli/types.d.ts +7 -0
  26. package/dist/lib/cli/types.js +1 -0
  27. package/dist/lib/cli/validate.d.ts +6 -0
  28. package/dist/lib/cli/validate.js +79 -0
  29. package/dist/lib/cli/vite-artifact.d.ts +13 -0
  30. package/dist/lib/cli/vite-artifact.js +213 -0
  31. package/dist/lib/react/components/AnnotatedCode.d.ts +19 -0
  32. package/dist/lib/react/components/AnnotatedCode.js +30 -0
  33. package/dist/lib/react/components/ArtifactState.d.ts +68 -0
  34. package/dist/lib/react/components/ArtifactState.js +286 -0
  35. package/dist/lib/react/components/Callout.d.ts +9 -0
  36. package/dist/lib/react/components/Callout.js +21 -0
  37. package/dist/lib/react/components/CodeBlock.d.ts +10 -0
  38. package/dist/lib/react/components/CodeBlock.js +28 -0
  39. package/dist/lib/react/components/Comments.d.ts +53 -0
  40. package/dist/lib/react/components/Comments.js +613 -0
  41. package/dist/lib/react/components/ComparisonSet.d.ts +24 -0
  42. package/dist/lib/react/components/ComparisonSet.js +30 -0
  43. package/dist/lib/react/components/DecisionMatrix.d.ts +16 -0
  44. package/dist/lib/react/components/DecisionMatrix.js +27 -0
  45. package/dist/lib/react/components/DiffBlock.d.ts +15 -0
  46. package/dist/lib/react/components/DiffBlock.js +24 -0
  47. package/dist/lib/react/components/ExportPanel.d.ts +7 -0
  48. package/dist/lib/react/components/ExportPanel.js +84 -0
  49. package/dist/lib/react/components/InlineText.d.ts +9 -0
  50. package/dist/lib/react/components/InlineText.js +18 -0
  51. package/dist/lib/react/components/Layout.d.ts +44 -0
  52. package/dist/lib/react/components/Layout.js +28 -0
  53. package/dist/lib/react/components/MarkdownBody.d.ts +7 -0
  54. package/dist/lib/react/components/MarkdownBody.js +36 -0
  55. package/dist/lib/react/components/OptionGrid.d.ts +13 -0
  56. package/dist/lib/react/components/OptionGrid.js +21 -0
  57. package/dist/lib/react/components/Section.d.ts +7 -0
  58. package/dist/lib/react/components/Section.js +41 -0
  59. package/dist/lib/react/components/SeverityBadge.d.ts +7 -0
  60. package/dist/lib/react/components/SeverityBadge.js +14 -0
  61. package/dist/lib/react/index.d.ts +33 -0
  62. package/dist/lib/react/index.js +16 -0
  63. package/dist/lib/react/registry.d.ts +24 -0
  64. package/dist/lib/react/registry.js +1002 -0
  65. package/dist/lib/react/styles.css +1417 -0
  66. package/docs/component-protocol.md +273 -0
  67. package/docs/component-taxonomy.md +273 -0
  68. package/docs/design.md +239 -0
  69. package/docs/design.zh-CN.md +217 -0
  70. package/docs/naming.md +123 -0
  71. package/docs/testing.md +138 -0
  72. package/package.json +90 -0
@@ -0,0 +1,24 @@
1
+ import path from "node:path";
2
+ import { loadConfig } from "./config.js";
3
+ import { createArtifactProject, startDevServer } from "./vite-artifact.js";
4
+ export async function devCommand(projectRoot, input) {
5
+ const config = await loadConfig(projectRoot);
6
+ const mdxPath = path.resolve(projectRoot, input);
7
+ const project = await createArtifactProject(projectRoot, mdxPath, config);
8
+ const server = await startDevServer(project);
9
+ const urls = server.resolvedUrls?.local ?? [];
10
+ console.log(`dev server ready: ${artifactUrl(urls[0] ?? `http://localhost:${config.port}/`, project.artifact.routePath)}`);
11
+ console.log("press Ctrl+C to stop");
12
+ const close = async () => {
13
+ await server.close();
14
+ await project.cleanup();
15
+ process.exit(0);
16
+ };
17
+ process.once("SIGINT", close);
18
+ process.once("SIGTERM", close);
19
+ }
20
+ function artifactUrl(baseUrl, routePath) {
21
+ const url = new URL(baseUrl);
22
+ url.pathname = routePath;
23
+ return url.toString();
24
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { buildCommand } from "./build.js";
4
+ import { componentsCommand } from "./components.js";
5
+ import { devCommand } from "./dev.js";
6
+ import { reviewCommand } from "./review.js";
7
+ import { initProject } from "./scaffold.js";
8
+ import { printValidationResult, validateMdx } from "./validate.js";
9
+ const projectRoot = process.cwd();
10
+ const [command, input] = process.argv.slice(2);
11
+ async function main() {
12
+ if (!command || command === "help" || command === "--help" || command === "-h") {
13
+ printHelp();
14
+ return;
15
+ }
16
+ if (command === "init") {
17
+ await initProject(projectRoot);
18
+ return;
19
+ }
20
+ if (command === "components") {
21
+ const args = process.argv.slice(3);
22
+ const json = args.includes("--json");
23
+ const name = args.find((arg) => arg !== "--json");
24
+ componentsCommand(name, { json });
25
+ return;
26
+ }
27
+ if (command === "review") {
28
+ await reviewCommand(projectRoot, process.argv.slice(3));
29
+ return;
30
+ }
31
+ if (!input) {
32
+ throw new Error(`${command} requires a .mdx file path.`);
33
+ }
34
+ if (command === "validate") {
35
+ const result = await validateMdx(path.resolve(projectRoot, input));
36
+ printValidationResult(result);
37
+ if (result.errors.length > 0) {
38
+ process.exitCode = 1;
39
+ }
40
+ return;
41
+ }
42
+ if (command === "build") {
43
+ await buildCommand(projectRoot, input);
44
+ return;
45
+ }
46
+ if (command === "dev") {
47
+ await devCommand(projectRoot, input);
48
+ return;
49
+ }
50
+ throw new Error(`Unknown command: ${command}`);
51
+ }
52
+ function printHelp() {
53
+ console.log(`artifact-kit
54
+
55
+ Usage:
56
+ artifact-kit init
57
+ artifact-kit components [ComponentName] [--json]
58
+ artifact-kit validate <file.mdx>
59
+ artifact-kit review add <file.mdx> --anchor <anchorId> --body <message> [--title <title>]
60
+ artifact-kit review reply <file.mdx> --thread <threadId> --body <message> [...repeat] [--status <status>]
61
+ artifact-kit review validate <file.mdx>
62
+ artifact-kit dev <file.mdx>
63
+ artifact-kit build <file.mdx>
64
+ `);
65
+ }
66
+ main().catch((error) => {
67
+ console.error(error instanceof Error ? error.message : String(error));
68
+ process.exitCode = 1;
69
+ });
@@ -0,0 +1,33 @@
1
+ type ReviewReply = {
2
+ threadId: string;
3
+ body: string;
4
+ };
5
+ type ReviewAddOptions = {
6
+ anchorId: string;
7
+ body: string;
8
+ title?: string;
9
+ };
10
+ type ReviewReplyOptions = {
11
+ replies: ReviewReply[];
12
+ status?: string;
13
+ };
14
+ export type ReviewValidationResult = {
15
+ anchorCount: number;
16
+ missingThreads: ReviewMissingThread[];
17
+ output: string;
18
+ threadCount: number;
19
+ };
20
+ export type ReviewMissingThread = {
21
+ anchorId: string;
22
+ threadId: string;
23
+ title?: string;
24
+ status?: string;
25
+ };
26
+ export declare function reviewCommand(projectRoot: string, args: string[]): Promise<void>;
27
+ export declare function addReviewThread(projectRoot: string, input: string, options: ReviewAddOptions): Promise<string>;
28
+ export declare function replyToReviewThread(projectRoot: string, input: string, options: ReviewReply & {
29
+ status?: string;
30
+ }): Promise<string>;
31
+ export declare function replyToReviewThreads(projectRoot: string, input: string, options: ReviewReplyOptions): Promise<string>;
32
+ export declare function validateReviewState(projectRoot: string, input: string): Promise<ReviewValidationResult>;
33
+ export {};
@@ -0,0 +1,390 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createArtifactRoute, readArtifactState, writeArtifactState } from "./artifact-state.js";
4
+ import { loadConfig } from "./config.js";
5
+ export async function reviewCommand(projectRoot, args) {
6
+ const [subcommand] = args;
7
+ if (subcommand === "add") {
8
+ const options = parseReviewAddArgs(args.slice(1));
9
+ console.log(await addReviewThread(projectRoot, options.input, options));
10
+ return;
11
+ }
12
+ if (subcommand === "reply") {
13
+ const options = parseReviewReplyArgs(args.slice(1));
14
+ console.log(await replyToReviewThreads(projectRoot, options.input, options));
15
+ return;
16
+ }
17
+ if (subcommand === "validate") {
18
+ const input = parseReviewValidateArgs(args.slice(1));
19
+ const result = await validateReviewState(projectRoot, input);
20
+ console.log(result.output);
21
+ if (result.missingThreads.length > 0) {
22
+ process.exitCode = 1;
23
+ }
24
+ return;
25
+ }
26
+ throw new Error("review requires a subcommand. Use `artifact-kit review add <file.mdx>`, `artifact-kit review reply <file.mdx>`, or `artifact-kit review validate <file.mdx>`.");
27
+ }
28
+ export async function addReviewThread(projectRoot, input, options) {
29
+ const artifact = await createArtifactFromInput(projectRoot, input);
30
+ const state = await readArtifactState(artifact);
31
+ const source = await readFile(artifact.sourcePath, "utf8");
32
+ const anchorIds = extractReviewAnchorIds(source);
33
+ if (!anchorIds.has(options.anchorId)) {
34
+ throw new Error(`Review anchor not found: ${options.anchorId}`);
35
+ }
36
+ if (state.threads.some((thread) => isRecord(thread) && thread.anchorId === options.anchorId)) {
37
+ throw new Error(`Review thread already exists for anchor: ${options.anchorId}`);
38
+ }
39
+ const threadId = createThreadId(options.anchorId, state.threads);
40
+ const message = createUserMessage(options.body);
41
+ const thread = {
42
+ id: threadId,
43
+ anchorId: options.anchorId,
44
+ status: "open",
45
+ ...(options.title ? { title: options.title } : {}),
46
+ messages: [message]
47
+ };
48
+ await writeArtifactState(projectRoot, artifact, {
49
+ ...state,
50
+ threads: [...state.threads, thread]
51
+ });
52
+ return [`review add ok`, `state: ${artifact.stateRelativePath}`, `thread: ${threadId}`, `anchorId: ${options.anchorId}`].join("\n");
53
+ }
54
+ export async function replyToReviewThread(projectRoot, input, options) {
55
+ return replyToReviewThreads(projectRoot, input, {
56
+ replies: [{ threadId: options.threadId, body: options.body }],
57
+ ...(options.status ? { status: options.status } : {})
58
+ });
59
+ }
60
+ export async function replyToReviewThreads(projectRoot, input, options) {
61
+ const artifact = await createArtifactFromInput(projectRoot, input);
62
+ const state = await readArtifactState(artifact);
63
+ const repliesByThread = groupRepliesByThread(options.replies);
64
+ const threadIds = new Set(state.threads.filter(isRecord).map((thread) => thread.id).filter(isString));
65
+ const missingThreadIds = Array.from(repliesByThread.keys()).filter((threadId) => !threadIds.has(threadId));
66
+ if (missingThreadIds.length > 0) {
67
+ throw new Error(`Review thread not found: ${missingThreadIds.join(", ")}`);
68
+ }
69
+ const messageRecords = [];
70
+ const threads = state.threads.map((thread) => {
71
+ if (!isRecord(thread) || !isString(thread.id)) {
72
+ return thread;
73
+ }
74
+ const replies = repliesByThread.get(thread.id);
75
+ if (!replies) {
76
+ return thread;
77
+ }
78
+ const messages = Array.isArray(thread.messages) ? thread.messages : [];
79
+ const nextMessages = replies.map((reply, index) => createAssistantMessage(reply.body, messageRecords.length + index));
80
+ for (const message of nextMessages) {
81
+ messageRecords.push({ threadId: thread.id, messageId: message.id });
82
+ }
83
+ return {
84
+ ...thread,
85
+ ...(options.status ? { status: options.status } : {}),
86
+ messages: [...messages, ...nextMessages]
87
+ };
88
+ });
89
+ await writeArtifactState(projectRoot, artifact, {
90
+ ...state,
91
+ threads
92
+ });
93
+ return [
94
+ `review reply ok`,
95
+ `state: ${artifact.stateRelativePath}`,
96
+ `messages: ${messageRecords.length}`,
97
+ ...messageRecords.map((record) => `- thread: ${record.threadId} message: ${record.messageId}`)
98
+ ].join("\n");
99
+ }
100
+ export async function validateReviewState(projectRoot, input) {
101
+ const artifact = await createArtifactFromInput(projectRoot, input);
102
+ const state = await readArtifactState(artifact);
103
+ const source = await readFile(artifact.sourcePath, "utf8");
104
+ const anchorIds = extractReviewAnchorIds(source);
105
+ const threads = state.threads.filter(isRecord);
106
+ const missingThreads = threads.flatMap((thread) => {
107
+ if (!isString(thread.anchorId) || anchorIds.has(thread.anchorId)) {
108
+ return [];
109
+ }
110
+ return [
111
+ {
112
+ anchorId: thread.anchorId,
113
+ threadId: isString(thread.id) ? thread.id : "(missing thread id)",
114
+ title: isString(thread.title) ? thread.title : undefined,
115
+ status: isString(thread.status) ? thread.status : undefined
116
+ }
117
+ ];
118
+ });
119
+ const output = missingThreads.length === 0
120
+ ? [
121
+ `review validate ok`,
122
+ `state: ${artifact.stateRelativePath}`,
123
+ `anchors: ${anchorIds.size}`,
124
+ `threads: ${threads.length}`
125
+ ].join("\n")
126
+ : [
127
+ `review validate failed`,
128
+ `state: ${artifact.stateRelativePath}`,
129
+ `anchors: ${anchorIds.size}`,
130
+ `threads: ${threads.length}`,
131
+ `missing: ${missingThreads.length}`,
132
+ ...missingThreads.map((thread) => [
133
+ `- thread: ${thread.threadId}`,
134
+ `anchorId: ${thread.anchorId}`,
135
+ thread.status ? `status: ${thread.status}` : undefined,
136
+ thread.title ? `title: ${thread.title}` : undefined
137
+ ]
138
+ .filter(Boolean)
139
+ .join(" "))
140
+ ].join("\n");
141
+ return {
142
+ anchorCount: anchorIds.size,
143
+ missingThreads,
144
+ output,
145
+ threadCount: threads.length
146
+ };
147
+ }
148
+ async function createArtifactFromInput(projectRoot, input) {
149
+ const config = await loadConfig(projectRoot);
150
+ const mdxPath = path.resolve(projectRoot, input);
151
+ return createArtifactRoute(projectRoot, mdxPath, config.docsDir);
152
+ }
153
+ function parseReviewReplyArgs(args) {
154
+ const [input, ...rest] = args;
155
+ if (!input) {
156
+ throw new Error("review reply requires a .mdx file path.");
157
+ }
158
+ const { replies, status } = parseReviewReplyOptions(rest);
159
+ return {
160
+ input,
161
+ replies,
162
+ ...(status ? { status } : {})
163
+ };
164
+ }
165
+ function parseReviewValidateArgs(args) {
166
+ const [input, ...rest] = args;
167
+ if (!input) {
168
+ throw new Error("review validate requires a .mdx file path.");
169
+ }
170
+ if (rest.length > 0) {
171
+ throw new Error(`Unexpected review validate argument: ${rest[0]}`);
172
+ }
173
+ return input;
174
+ }
175
+ function parseReviewAddArgs(args) {
176
+ const [input, ...rest] = args;
177
+ if (!input) {
178
+ throw new Error("review add requires a .mdx file path.");
179
+ }
180
+ const options = parseKeyValueOptions(rest, ["anchor", "body", "title"]);
181
+ const anchorId = options.get("anchor");
182
+ const body = options.get("body");
183
+ const title = options.get("title");
184
+ if (!anchorId) {
185
+ throw new Error("review add requires --anchor <anchorId>.");
186
+ }
187
+ if (!body || !body.trim()) {
188
+ throw new Error("review add requires --body <message>.");
189
+ }
190
+ return {
191
+ input,
192
+ anchorId,
193
+ body: body.trim(),
194
+ ...(title ? { title } : {})
195
+ };
196
+ }
197
+ function parseKeyValueOptions(args, allowedKeys) {
198
+ const options = new Map();
199
+ for (let index = 0; index < args.length; index += 1) {
200
+ const name = args[index];
201
+ if (!name?.startsWith("--")) {
202
+ throw new Error(`Unexpected review argument: ${name}`);
203
+ }
204
+ const key = name.slice(2);
205
+ const value = args[index + 1];
206
+ if (!allowedKeys.includes(key)) {
207
+ throw new Error(`Unknown review option: --${key}.`);
208
+ }
209
+ if (!value || value.startsWith("--")) {
210
+ throw new Error(`Missing value for --${key}.`);
211
+ }
212
+ options.set(key, value);
213
+ index += 1;
214
+ }
215
+ return options;
216
+ }
217
+ function parseReviewReplyOptions(args) {
218
+ const replies = [];
219
+ let status;
220
+ let threadId;
221
+ for (let index = 0; index < args.length; index += 1) {
222
+ const name = args[index];
223
+ if (!name?.startsWith("--")) {
224
+ throw new Error(`Unexpected review reply argument: ${name}`);
225
+ }
226
+ const key = name.slice(2);
227
+ const value = args[index + 1];
228
+ if (!value || value.startsWith("--")) {
229
+ throw new Error(`Missing value for --${key}.`);
230
+ }
231
+ if (key === "thread") {
232
+ if (threadId) {
233
+ throw new Error(`Missing --body for --thread ${threadId}.`);
234
+ }
235
+ threadId = value;
236
+ }
237
+ else if (key === "body") {
238
+ if (!threadId) {
239
+ throw new Error("review reply requires --thread before --body.");
240
+ }
241
+ if (!value.trim()) {
242
+ throw new Error("review reply requires --body <message>.");
243
+ }
244
+ replies.push({ threadId, body: value.trim() });
245
+ threadId = undefined;
246
+ }
247
+ else if (key === "status") {
248
+ status = value;
249
+ }
250
+ else {
251
+ throw new Error(`Unknown review reply option: --${key}.`);
252
+ }
253
+ index += 1;
254
+ }
255
+ if (threadId) {
256
+ throw new Error(`Missing --body for --thread ${threadId}.`);
257
+ }
258
+ if (replies.length === 0) {
259
+ throw new Error("review reply requires at least one --thread <threadId> --body <message> pair.");
260
+ }
261
+ return { replies, status };
262
+ }
263
+ function groupRepliesByThread(replies) {
264
+ const repliesByThread = new Map();
265
+ for (const reply of replies) {
266
+ const threadReplies = repliesByThread.get(reply.threadId) ?? [];
267
+ threadReplies.push(reply);
268
+ repliesByThread.set(reply.threadId, threadReplies);
269
+ }
270
+ return repliesByThread;
271
+ }
272
+ function createAssistantMessage(body, index = 0) {
273
+ return {
274
+ id: `msg_${Date.now().toString(36)}_${index.toString(36)}`,
275
+ role: "assistant",
276
+ body,
277
+ createdAt: new Date().toISOString()
278
+ };
279
+ }
280
+ function createUserMessage(body) {
281
+ return {
282
+ id: `msg_${Date.now().toString(36)}_0`,
283
+ role: "user",
284
+ body,
285
+ createdAt: new Date().toISOString()
286
+ };
287
+ }
288
+ function createThreadId(anchorId, threads) {
289
+ const existingIds = new Set(threads.filter(isRecord).map((thread) => thread.id).filter(isString));
290
+ const base = `thr_${compactId(anchorId)}`;
291
+ if (!existingIds.has(base)) {
292
+ return base;
293
+ }
294
+ for (let index = 2; index < 100; index += 1) {
295
+ const candidate = `${base}_${index}`;
296
+ if (!existingIds.has(candidate)) {
297
+ return candidate;
298
+ }
299
+ }
300
+ return `thr_${compactId(anchorId)}_${Date.now().toString(36)}`;
301
+ }
302
+ function extractReviewAnchorIds(source) {
303
+ const anchorIds = new Set();
304
+ for (const match of source.matchAll(/<([A-Z][A-Za-z0-9.]*)\b[^>]*(?:\sid|\stargetId)\s*=\s*["']([^"']+)["']/g)) {
305
+ if (match[2]) {
306
+ anchorIds.add(match[2]);
307
+ }
308
+ }
309
+ addArrayChildAnchors(source, anchorIds, "DecisionMatrix", "options");
310
+ addArrayChildAnchors(source, anchorIds, "OptionGrid", "options");
311
+ addArrayChildAnchors(source, anchorIds, "AnnotatedCode", "annotations", { addCodeChild: true });
312
+ addComparisonSetChildAnchors(source, anchorIds);
313
+ return anchorIds;
314
+ }
315
+ function addArrayChildAnchors(source, anchorIds, componentName, arrayPropName, options = {}) {
316
+ const componentPattern = new RegExp(`<${componentName}\\b[\\s\\S]*?\\/>`, "g");
317
+ for (const match of source.matchAll(componentPattern)) {
318
+ const componentSource = match[0];
319
+ const parentId = extractJsxStringProp(componentSource, "id");
320
+ if (!parentId) {
321
+ continue;
322
+ }
323
+ if (options.addCodeChild) {
324
+ anchorIds.add(`${parentId}.code`);
325
+ }
326
+ const arraySource = extractJsxArrayProp(componentSource, arrayPropName);
327
+ if (!arraySource) {
328
+ continue;
329
+ }
330
+ for (const itemId of extractObjectIds(arraySource)) {
331
+ anchorIds.add(`${parentId}.${itemId}`);
332
+ }
333
+ }
334
+ }
335
+ function addComparisonSetChildAnchors(source, anchorIds) {
336
+ for (const match of source.matchAll(/<ComparisonSet\b[\s\S]*?<\/ComparisonSet>/g)) {
337
+ const componentSource = match[0];
338
+ const parentId = extractJsxStringProp(componentSource, "id");
339
+ if (!parentId) {
340
+ continue;
341
+ }
342
+ for (const itemMatch of componentSource.matchAll(/<ComparisonSet\.Item\b[^>]*\sid\s*=\s*["']([^"']+)["'][^>]*>/g)) {
343
+ if (itemMatch[1]) {
344
+ anchorIds.add(`${parentId}.${itemMatch[1]}`);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ function extractJsxStringProp(source, propName) {
350
+ const pattern = new RegExp(`\\b${propName}\\s*=\\s*["']([^"']+)["']`);
351
+ return source.match(pattern)?.[1];
352
+ }
353
+ function extractJsxArrayProp(source, propName) {
354
+ const propStart = source.search(new RegExp(`\\b${propName}\\s*=\\s*\\{\\s*\\[`));
355
+ if (propStart < 0) {
356
+ return undefined;
357
+ }
358
+ const arrayStart = source.indexOf("[", propStart);
359
+ let depth = 0;
360
+ for (let index = arrayStart; index < source.length; index += 1) {
361
+ const char = source[index];
362
+ if (char === "[") {
363
+ depth += 1;
364
+ }
365
+ else if (char === "]") {
366
+ depth -= 1;
367
+ if (depth === 0) {
368
+ return source.slice(arrayStart, index + 1);
369
+ }
370
+ }
371
+ }
372
+ return undefined;
373
+ }
374
+ function extractObjectIds(source) {
375
+ return Array.from(source.matchAll(/\bid\s*:\s*["']([^"']+)["']/g), (match) => match[1]).filter(Boolean);
376
+ }
377
+ function compactId(value) {
378
+ const compact = value
379
+ .toLowerCase()
380
+ .replace(/[^a-z0-9]+/g, "_")
381
+ .replace(/^_+|_+$/g, "")
382
+ .slice(0, 32);
383
+ return compact || "thread";
384
+ }
385
+ function isRecord(value) {
386
+ return typeof value === "object" && value !== null && !Array.isArray(value);
387
+ }
388
+ function isString(value) {
389
+ return typeof value === "string";
390
+ }
@@ -0,0 +1 @@
1
+ export declare function initProject(projectRoot: string): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function initProject(projectRoot) {
4
+ const docsDir = path.join(projectRoot, "artifact-docs", "examples");
5
+ const agentsDir = path.join(projectRoot, "agents");
6
+ await mkdir(docsDir, { recursive: true });
7
+ await mkdir(agentsDir, { recursive: true });
8
+ await writeFile(path.join(projectRoot, "artifact-kit.config.mjs"), `/** @type {import("mdx-artifacts").ArtifactKitConfig} */
9
+ const config = {
10
+ docsDir: "artifact-docs",
11
+ outDir: "dist/artifacts",
12
+ includeDefaultStyles: true,
13
+ styles: []
14
+ };
15
+
16
+ export default config;
17
+ `, { flag: "wx" }).catch(ignoreExisting);
18
+ await writeFile(path.join(agentsDir, "AGENTS.snippet.md"), `# Artifact Kit Agent Instructions
19
+
20
+ 1. Create .mdx files under artifact-docs/.
21
+ 2. Prefer Artifact Kit high-level components. Do not generate raw HTML unless explicitly requested.
22
+ 3. Interactive artifacts must include ExportPanel or an equivalent export path.
23
+ 4. Run artifact-kit components <ComponentName> when component props are unclear.
24
+ 5. Run artifact-kit validate <file.mdx> before build.
25
+ 6. Run artifact-kit build <file.mdx> to produce standalone HTML.
26
+ `, { flag: "wx" }).catch(ignoreExisting);
27
+ await writeFile(path.join(docsDir, "hello.mdx"), `import { DecisionMatrix, ExportPanel } from "mdx-artifacts/react";
28
+
29
+ # Hello Artifact
30
+
31
+ <DecisionMatrix
32
+ id="decision.initialized"
33
+ question="Has Artifact Kit been initialized?"
34
+ options={[
35
+ {
36
+ name: "Initialized",
37
+ pros: ["MDX source exists", "Export panel exists"],
38
+ cons: ["Real content still needs to be added"],
39
+ verdict: "Ready to continue generating artifacts"
40
+ }
41
+ ]}
42
+ />
43
+
44
+ <ExportPanel
45
+ value={{
46
+ recommendation: "Continue generating interactive HTML artifacts with MDX and high-level components"
47
+ }}
48
+ />
49
+ `, { flag: "wx" }).catch(ignoreExisting);
50
+ console.log("Artifact Kit initialized.");
51
+ }
52
+ function ignoreExisting(error) {
53
+ if (error.code !== "EEXIST") {
54
+ throw error;
55
+ }
56
+ }
@@ -0,0 +1,7 @@
1
+ export type ArtifactKitConfig = {
2
+ docsDir?: string;
3
+ includeDefaultStyles?: boolean;
4
+ outDir?: string;
5
+ port?: number;
6
+ styles?: string[];
7
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export type ValidationResult = {
2
+ errors: string[];
3
+ warnings: string[];
4
+ };
5
+ export declare function validateMdx(filePath: string): Promise<ValidationResult>;
6
+ export declare function printValidationResult(result: ValidationResult): void;
@@ -0,0 +1,79 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { componentRegistry } from "../react/registry.js";
4
+ const componentsRequiringStableId = [
5
+ "Section",
6
+ "DecisionMatrix",
7
+ "OptionGrid",
8
+ "ComparisonSet",
9
+ "ComparisonSet.Item",
10
+ "AnnotatedCode",
11
+ "CodeBlock",
12
+ "DiffBlock",
13
+ "Callout"
14
+ ];
15
+ export async function validateMdx(filePath) {
16
+ const result = { errors: [], warnings: [] };
17
+ if (path.extname(filePath) !== ".mdx") {
18
+ result.errors.push("Input file must be .mdx.");
19
+ return result;
20
+ }
21
+ let source = "";
22
+ try {
23
+ source = await readFile(filePath, "utf8");
24
+ }
25
+ catch {
26
+ result.errors.push(`Failed to read file: ${filePath}`);
27
+ return result;
28
+ }
29
+ if (source.includes("<script")) {
30
+ result.errors.push("Do not write <script> directly in MDX. Wrap behavior in a controlled component.");
31
+ }
32
+ if (!source.includes("ExportPanel") && !source.includes("CommentExport")) {
33
+ result.warnings.push("ExportPanel or equivalent export component not found. Interactive artifacts should provide an export path.");
34
+ }
35
+ if (source.length > 40_000) {
36
+ result.warnings.push("MDX file is large. Move bulky data to adjacent JSON files.");
37
+ }
38
+ const knownComponents = componentRegistry.map((component) => component.name);
39
+ const usedArtifactComponent = knownComponents.some((name) => source.includes(`<${name}`));
40
+ if (!usedArtifactComponent) {
41
+ result.warnings.push("No first-stage high-level artifact component found. Confirm plain MDX is intentional.");
42
+ }
43
+ const sourceWithoutStringLiterals = stripStringLiterals(source);
44
+ for (const componentName of componentsRequiringStableId) {
45
+ if (hasOpeningTagWithoutProp(sourceWithoutStringLiterals, componentName, "id")) {
46
+ result.warnings.push(`${componentName} should include a stable id prop so comments and state can use a durable anchorId.`);
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+ export function printValidationResult(result) {
52
+ for (const error of result.errors) {
53
+ console.error(`error: ${error}`);
54
+ }
55
+ for (const warning of result.warnings) {
56
+ console.warn(`warn: ${warning}`);
57
+ }
58
+ if (result.errors.length === 0 && result.warnings.length === 0) {
59
+ console.log("validate ok");
60
+ }
61
+ }
62
+ function stripStringLiterals(source) {
63
+ return source
64
+ .replace(/`(?:\\[\s\S]|[^`\\])*`/g, "``")
65
+ .replace(/"(?:\\.|[^"\\])*"/g, '""')
66
+ .replace(/'(?:\\.|[^'\\])*'/g, "''");
67
+ }
68
+ function hasOpeningTagWithoutProp(source, componentName, propName) {
69
+ const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
70
+ const tagPattern = new RegExp(`<${escapedName}(?=[\\s>/])[^>]*>`, "g");
71
+ const propPattern = new RegExp(`\\s${propName}\\s*=`);
72
+ for (const match of source.matchAll(tagPattern)) {
73
+ const openingTag = match[0];
74
+ if (!propPattern.test(openingTag)) {
75
+ return true;
76
+ }
77
+ }
78
+ return false;
79
+ }