mdx-artifacts 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -59
- package/README.zh-CN.md +299 -42
- package/agents/AGENTS.snippet.md +21 -10
- package/artifact-docs/examples/commentable-feedback.mdx +76 -70
- package/artifact-docs/examples/decision-matrix.mdx +122 -50
- package/artifact-docs/examples/layout-composition.mdx +106 -128
- package/artifact-docs/examples/streamlit-style-mixed.mdx +100 -85
- package/dist/lib/cli/{build.js → commands/build.js} +2 -2
- package/dist/lib/cli/{components.js → commands/components.js} +19 -3
- package/dist/lib/cli/{dev.js → commands/dev.js} +2 -2
- package/dist/lib/cli/commands/interactions.d.ts +2 -0
- package/dist/lib/cli/commands/interactions.js +280 -0
- package/dist/lib/cli/commands/review.d.ts +1 -0
- package/dist/lib/cli/commands/review.js +171 -0
- package/dist/lib/cli/commands/scaffold.d.ts +16 -0
- package/dist/lib/cli/commands/scaffold.js +440 -0
- package/dist/lib/cli/commands/validate.d.ts +18 -0
- package/dist/lib/cli/commands/validate.js +311 -0
- package/dist/lib/cli/config/config.d.ts +2 -0
- package/dist/lib/cli/{config.js → config/config.js} +3 -2
- package/dist/lib/cli/{types.d.ts → config/types.d.ts} +2 -1
- package/dist/lib/cli/{vite-artifact.d.ts → dev-server/vite-artifact.d.ts} +3 -3
- package/dist/lib/cli/{vite-artifact.js → dev-server/vite-artifact.js} +170 -10
- package/dist/lib/cli/diagnostics/diagnostics.d.ts +11 -0
- package/dist/lib/cli/diagnostics/diagnostics.js +1 -0
- package/dist/lib/cli/index.js +39 -18
- package/dist/lib/cli/mdx/sortable-list.d.ts +14 -0
- package/dist/lib/cli/mdx/sortable-list.js +520 -0
- package/dist/lib/cli/resources/resource-policy.d.ts +15 -0
- package/dist/lib/cli/resources/resource-policy.js +46 -0
- package/dist/lib/cli/resources/safe-path.d.ts +13 -0
- package/dist/lib/cli/resources/safe-path.js +55 -0
- package/dist/lib/cli/services/interaction-service.d.ts +40 -0
- package/dist/lib/cli/services/interaction-service.js +226 -0
- package/dist/lib/cli/services/review.d.ts +43 -0
- package/dist/lib/cli/{review.js → services/review.js} +34 -172
- package/dist/lib/react/{components → composites/comparison-set}/ComparisonSet.d.ts +1 -1
- package/dist/lib/react/{components → composites/comparison-set}/ComparisonSet.js +3 -3
- package/dist/lib/react/composites/comparison-set/index.d.ts +2 -0
- package/dist/lib/react/composites/comparison-set/index.js +1 -0
- package/dist/lib/react/composites/content-set/ContentItem.d.ts +37 -0
- package/dist/lib/react/composites/content-set/ContentItem.js +49 -0
- package/dist/lib/react/composites/content-set/index.d.ts +2 -0
- package/dist/lib/react/composites/content-set/index.js +1 -0
- package/dist/lib/react/{components → composites/export-panel}/ExportPanel.js +2 -2
- package/dist/lib/react/composites/export-panel/index.d.ts +2 -0
- package/dist/lib/react/composites/export-panel/index.js +1 -0
- package/dist/lib/react/{components → composites/section}/Section.js +1 -1
- package/dist/lib/react/composites/section/index.d.ts +2 -0
- package/dist/lib/react/composites/section/index.js +1 -0
- package/dist/lib/react/index.d.ts +36 -31
- package/dist/lib/react/index.js +18 -15
- package/dist/lib/react/interactions/artifact-state/index.d.ts +1 -0
- package/dist/lib/react/interactions/artifact-state/index.js +1 -0
- package/dist/lib/react/{components → interactions/comments}/Comments.d.ts +2 -2
- package/dist/lib/react/{components → interactions/comments}/Comments.js +3 -3
- package/dist/lib/react/interactions/comments/index.d.ts +1 -0
- package/dist/lib/react/interactions/comments/index.js +1 -0
- package/dist/lib/react/interactions/sortable-list/SortableList.d.ts +29 -0
- package/dist/lib/react/interactions/sortable-list/SortableList.js +282 -0
- package/dist/lib/react/interactions/sortable-list/index.d.ts +1 -0
- package/dist/lib/react/interactions/sortable-list/index.js +1 -0
- package/dist/lib/react/layout/layout-primitives/index.d.ts +2 -0
- package/dist/lib/react/layout/layout-primitives/index.js +1 -0
- package/dist/lib/react/legacy/LegacyContentComponents.d.ts +65 -0
- package/dist/lib/react/legacy/LegacyContentComponents.js +26 -0
- package/dist/lib/react/mdx-components.d.ts +5 -0
- package/dist/lib/react/mdx-components.js +38 -0
- package/dist/lib/react/{components → primitives/annotated-code}/AnnotatedCode.d.ts +1 -1
- package/dist/lib/react/{components → primitives/annotated-code}/AnnotatedCode.js +5 -5
- package/dist/lib/react/primitives/annotated-code/index.d.ts +2 -0
- package/dist/lib/react/primitives/annotated-code/index.js +1 -0
- package/dist/lib/react/primitives/callout/Callout.d.ts +11 -0
- package/dist/lib/react/{components → primitives/callout}/Callout.js +9 -6
- package/dist/lib/react/primitives/callout/index.d.ts +2 -0
- package/dist/lib/react/primitives/callout/index.js +1 -0
- package/dist/lib/react/primitives/code-block/CodeBlock.d.ts +20 -0
- package/dist/lib/react/primitives/code-block/CodeBlock.js +32 -0
- package/dist/lib/react/primitives/code-block/index.d.ts +2 -0
- package/dist/lib/react/primitives/code-block/index.js +1 -0
- package/dist/lib/react/primitives/code-surface/CodeSurface.d.ts +11 -0
- package/dist/lib/react/primitives/code-surface/CodeSurface.js +34 -0
- package/dist/lib/react/primitives/code-surface/index.d.ts +2 -0
- package/dist/lib/react/primitives/code-surface/index.js +1 -0
- package/dist/lib/react/primitives/diff-block/DiffBlock.js +25 -0
- package/dist/lib/react/primitives/diff-block/index.d.ts +2 -0
- package/dist/lib/react/primitives/diff-block/index.js +1 -0
- package/dist/lib/react/{components → primitives/inline-text}/InlineText.d.ts +4 -2
- package/dist/lib/react/primitives/inline-text/InlineText.js +28 -0
- package/dist/lib/react/primitives/inline-text/index.d.ts +2 -0
- package/dist/lib/react/primitives/inline-text/index.js +1 -0
- package/dist/lib/react/primitives/markdown-body/MarkdownBody.d.ts +9 -0
- package/dist/lib/react/primitives/markdown-body/MarkdownBody.js +49 -0
- package/dist/lib/react/primitives/markdown-body/index.d.ts +2 -0
- package/dist/lib/react/primitives/markdown-body/index.js +1 -0
- package/dist/lib/react/primitives/severity-badge/index.d.ts +2 -0
- package/dist/lib/react/primitives/severity-badge/index.js +1 -0
- package/dist/lib/react/registry.d.ts +10 -0
- package/dist/lib/react/registry.js +505 -210
- package/dist/lib/react/styles.css +490 -38
- package/docs/cli-structure.md +141 -0
- package/docs/component-protocol.md +199 -33
- package/docs/component-taxonomy.md +40 -4
- package/docs/design.md +42 -21
- package/docs/design.zh-CN.md +41 -21
- package/docs/naming.md +17 -7
- package/docs/releasing.md +132 -0
- package/docs/testing.md +35 -10
- package/package.json +9 -7
- package/dist/lib/cli/config.d.ts +0 -2
- package/dist/lib/cli/review.d.ts +0 -33
- package/dist/lib/cli/scaffold.d.ts +0 -1
- package/dist/lib/cli/scaffold.js +0 -56
- package/dist/lib/cli/validate.d.ts +0 -6
- package/dist/lib/cli/validate.js +0 -79
- package/dist/lib/react/components/Callout.d.ts +0 -9
- package/dist/lib/react/components/CodeBlock.d.ts +0 -10
- package/dist/lib/react/components/CodeBlock.js +0 -28
- package/dist/lib/react/components/DecisionMatrix.d.ts +0 -16
- package/dist/lib/react/components/DecisionMatrix.js +0 -27
- package/dist/lib/react/components/DiffBlock.js +0 -24
- package/dist/lib/react/components/InlineText.js +0 -18
- package/dist/lib/react/components/MarkdownBody.d.ts +0 -7
- package/dist/lib/react/components/MarkdownBody.js +0 -36
- package/dist/lib/react/components/OptionGrid.d.ts +0 -13
- package/dist/lib/react/components/OptionGrid.js +0 -21
- /package/dist/lib/cli/{build.d.ts → commands/build.d.ts} +0 -0
- /package/dist/lib/cli/{components.d.ts → commands/components.d.ts} +0 -0
- /package/dist/lib/cli/{dev.d.ts → commands/dev.d.ts} +0 -0
- /package/dist/lib/cli/{types.js → config/types.js} +0 -0
- /package/dist/lib/cli/{artifact-state.d.ts → state/artifact-state.d.ts} +0 -0
- /package/dist/lib/cli/{artifact-state.js → state/artifact-state.js} +0 -0
- /package/dist/lib/react/{components → composites/export-panel}/ExportPanel.d.ts +0 -0
- /package/dist/lib/react/{components → composites/section}/Section.d.ts +0 -0
- /package/dist/lib/react/{components → interactions/artifact-state}/ArtifactState.d.ts +0 -0
- /package/dist/lib/react/{components → interactions/artifact-state}/ArtifactState.js +0 -0
- /package/dist/lib/react/{components → layout/layout-primitives}/Layout.d.ts +0 -0
- /package/dist/lib/react/{components → layout/layout-primitives}/Layout.js +0 -0
- /package/dist/lib/react/{components → primitives/diff-block}/DiffBlock.d.ts +0 -0
- /package/dist/lib/react/{components → primitives/severity-badge}/SeverityBadge.d.ts +0 -0
- /package/dist/lib/react/{components → primitives/severity-badge}/SeverityBadge.js +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { componentRegistry } from "../../react/registry.js";
|
|
4
|
+
import { validateResourceReferences } from "../resources/resource-policy.js";
|
|
5
|
+
const componentsRequiringStableId = [
|
|
6
|
+
"Section",
|
|
7
|
+
"ComparisonSet",
|
|
8
|
+
"ComparisonSet.Item",
|
|
9
|
+
"AnnotatedCode",
|
|
10
|
+
"CodeBlock",
|
|
11
|
+
"DiffBlock",
|
|
12
|
+
"Callout",
|
|
13
|
+
"ContentItem",
|
|
14
|
+
"ContentSet",
|
|
15
|
+
"ContentSet.Item",
|
|
16
|
+
"SortableList"
|
|
17
|
+
];
|
|
18
|
+
const deprecatedAuthoringComponents = [
|
|
19
|
+
{
|
|
20
|
+
componentName: "DecisionMatrix",
|
|
21
|
+
warning: "DecisionMatrix is a deprecated compatibility component. Use ContentSet with ContentSet.Item children.",
|
|
22
|
+
suggestion: "Replace DecisionMatrix with ContentSet and ContentSet.Item children."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
componentName: "DecisionMatrix.Option",
|
|
26
|
+
warning: "DecisionMatrix.Option is a deprecated compatibility component. Use ContentSet.Item.",
|
|
27
|
+
suggestion: "Replace DecisionMatrix.Option with ContentSet.Item."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
componentName: "OptionGrid",
|
|
31
|
+
warning: "OptionGrid is a deprecated compatibility component. Use ContentSet with ContentSet.Item children.",
|
|
32
|
+
suggestion: "Replace OptionGrid with ContentSet and ContentSet.Item children."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
componentName: "OptionGrid.Item",
|
|
36
|
+
warning: "OptionGrid.Item is a deprecated compatibility component. Use ContentSet.Item.",
|
|
37
|
+
suggestion: "Replace OptionGrid.Item with ContentSet.Item."
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
const deprecatedAuthoringProps = [
|
|
41
|
+
{
|
|
42
|
+
componentName: "DecisionMatrix",
|
|
43
|
+
propName: "question",
|
|
44
|
+
warning: 'DecisionMatrix prop "question" is deprecated. Use ContentSet prop "title".',
|
|
45
|
+
suggestion: 'Rename "question" to "title" when migrating to ContentSet.'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
componentName: "DecisionMatrix",
|
|
49
|
+
propName: "options",
|
|
50
|
+
warning: 'DecisionMatrix prop "options" is deprecated. Use ContentSet.Item children.',
|
|
51
|
+
suggestion: "Move each option into an explicit ContentSet.Item child."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
componentName: "DecisionMatrix.Option",
|
|
55
|
+
propName: "name",
|
|
56
|
+
warning: 'DecisionMatrix.Option prop "name" is deprecated. Use "title".',
|
|
57
|
+
suggestion: 'Rename "name" to "title".'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
componentName: "DecisionMatrix.Option",
|
|
61
|
+
propName: "pros",
|
|
62
|
+
warning: 'DecisionMatrix.Option prop "pros" is deprecated. Move long lists into MDX children.',
|
|
63
|
+
suggestion: "Move pros into the ContentSet.Item MDX body."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
componentName: "DecisionMatrix.Option",
|
|
67
|
+
propName: "cons",
|
|
68
|
+
warning: 'DecisionMatrix.Option prop "cons" is deprecated. Move long lists into MDX children.',
|
|
69
|
+
suggestion: "Move cons into the ContentSet.Item MDX body."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
componentName: "DecisionMatrix.Option",
|
|
73
|
+
propName: "risks",
|
|
74
|
+
warning: 'DecisionMatrix.Option prop "risks" is deprecated. Move risks into MDX children.',
|
|
75
|
+
suggestion: "Move risks into the ContentSet.Item MDX body."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
componentName: "DecisionMatrix.Option",
|
|
79
|
+
propName: "confidence",
|
|
80
|
+
warning: 'DecisionMatrix.Option prop "confidence" is deprecated. Use "badge" for short display labels.',
|
|
81
|
+
suggestion: 'Use "badge" for short display labels.'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
componentName: "DecisionMatrix.Option",
|
|
85
|
+
propName: "verdict",
|
|
86
|
+
warning: 'DecisionMatrix.Option prop "verdict" is deprecated. Use "summary" or MDX children.',
|
|
87
|
+
suggestion: 'Use "summary" for short verdict text or move longer verdicts into children.'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
componentName: "OptionGrid.Item",
|
|
91
|
+
propName: "name",
|
|
92
|
+
warning: 'OptionGrid.Item prop "name" is deprecated. Use "title".',
|
|
93
|
+
suggestion: 'Rename "name" to "title".'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
componentName: "OptionGrid",
|
|
97
|
+
propName: "options",
|
|
98
|
+
warning: 'OptionGrid prop "options" is deprecated. Use ContentSet.Item children.',
|
|
99
|
+
suggestion: "Move each option into an explicit ContentSet.Item child."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
componentName: "OptionGrid.Item",
|
|
103
|
+
propName: "intent",
|
|
104
|
+
warning: 'OptionGrid.Item prop "intent" is deprecated. Use "summary" for short intent text.',
|
|
105
|
+
suggestion: 'Rename "intent" to "summary" when the text is short.'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
componentName: "OptionGrid.Item",
|
|
109
|
+
propName: "description",
|
|
110
|
+
warning: 'OptionGrid.Item prop "description" is deprecated. Move longer descriptions into MDX children.',
|
|
111
|
+
suggestion: "Move longer descriptions into the ContentSet.Item MDX body."
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
componentName: "OptionGrid.Item",
|
|
115
|
+
propName: "tradeoffs",
|
|
116
|
+
warning: 'OptionGrid.Item prop "tradeoffs" is deprecated. Move tradeoff lists into MDX children.',
|
|
117
|
+
suggestion: "Move tradeoff lists into the ContentSet.Item MDX body."
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
export async function validateMdx(filePath, options = {}) {
|
|
121
|
+
const result = createValidationResult();
|
|
122
|
+
if (path.extname(filePath) !== ".mdx") {
|
|
123
|
+
addDiagnostic(result, {
|
|
124
|
+
severity: "error",
|
|
125
|
+
code: "invalid_file_extension",
|
|
126
|
+
message: "Input file must be .mdx.",
|
|
127
|
+
sourcePath: filePath,
|
|
128
|
+
suggestion: "Pass a .mdx artifact source file to mdx-artifacts validate.",
|
|
129
|
+
example: "mdx-artifacts validate artifact-docs/examples/hello.mdx"
|
|
130
|
+
});
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
let source = "";
|
|
134
|
+
try {
|
|
135
|
+
source = await readFile(filePath, "utf8");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
addDiagnostic(result, {
|
|
139
|
+
severity: "error",
|
|
140
|
+
code: "file_read_failed",
|
|
141
|
+
message: `Failed to read file: ${filePath}`,
|
|
142
|
+
sourcePath: filePath,
|
|
143
|
+
suggestion: "Confirm the file exists and is readable."
|
|
144
|
+
});
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
if (source.includes("<script")) {
|
|
148
|
+
addDiagnostic(result, {
|
|
149
|
+
severity: "error",
|
|
150
|
+
code: "raw_script_blocked",
|
|
151
|
+
message: "Do not write <script> directly in MDX. Wrap behavior in a controlled component.",
|
|
152
|
+
sourcePath: filePath,
|
|
153
|
+
suggestion: "Move browser behavior into a controlled React component instead of inline script tags."
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (options.config) {
|
|
157
|
+
const resourceDiagnostics = await validateResourceReferences({
|
|
158
|
+
projectRoot: options.projectRoot ?? process.cwd(),
|
|
159
|
+
references: options.config.styles.map((stylePath) => ({
|
|
160
|
+
type: "style",
|
|
161
|
+
path: stylePath,
|
|
162
|
+
sourcePath: filePath,
|
|
163
|
+
fieldName: "styles"
|
|
164
|
+
}))
|
|
165
|
+
});
|
|
166
|
+
for (const diagnostic of resourceDiagnostics) {
|
|
167
|
+
addDiagnostic(result, diagnostic);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!source.includes("ExportPanel") && !source.includes("CommentExport")) {
|
|
171
|
+
addDiagnostic(result, {
|
|
172
|
+
severity: "warning",
|
|
173
|
+
code: "export_component_missing",
|
|
174
|
+
message: "ExportPanel or equivalent export component not found. Interactive artifacts should provide an export path.",
|
|
175
|
+
sourcePath: filePath,
|
|
176
|
+
suggestion: "Add ExportPanel for result handoff, or CommentExport for comments-only artifacts.",
|
|
177
|
+
example: '<ExportPanel value={{ status: "ready" }} />'
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (source.length > 40_000) {
|
|
181
|
+
addDiagnostic(result, {
|
|
182
|
+
severity: "warning",
|
|
183
|
+
code: "mdx_file_large",
|
|
184
|
+
message: "MDX file is large. Move bulky data to adjacent JSON files.",
|
|
185
|
+
sourcePath: filePath,
|
|
186
|
+
suggestion: "Keep authored prose in MDX and move large structured data into adjacent files."
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const knownComponents = componentRegistry.map((component) => component.name);
|
|
190
|
+
const usedArtifactComponent = knownComponents.some((name) => source.includes(`<${name}`));
|
|
191
|
+
if (!usedArtifactComponent) {
|
|
192
|
+
addDiagnostic(result, {
|
|
193
|
+
severity: "warning",
|
|
194
|
+
code: "high_level_component_missing",
|
|
195
|
+
message: "No first-stage high-level artifact component found. Confirm plain MDX is intentional.",
|
|
196
|
+
sourcePath: filePath,
|
|
197
|
+
suggestion: "Use high-level artifact components when the document needs structured workflow UI."
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const sourceWithoutStringLiterals = stripStringLiterals(source);
|
|
201
|
+
for (const componentName of componentsRequiringStableId) {
|
|
202
|
+
if (hasOpeningTagWithoutProp(sourceWithoutStringLiterals, componentName, "id")) {
|
|
203
|
+
addDiagnostic(result, {
|
|
204
|
+
severity: "warning",
|
|
205
|
+
code: "stable_id_missing",
|
|
206
|
+
message: `${componentName} should include a stable id prop so comments and state can use a durable anchorId.`,
|
|
207
|
+
sourcePath: filePath,
|
|
208
|
+
componentName,
|
|
209
|
+
propName: "id",
|
|
210
|
+
suggestion: `Add a stable id prop to ${componentName}.`,
|
|
211
|
+
example: stableIdExample(componentName)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const deprecatedComponent of deprecatedAuthoringComponents) {
|
|
216
|
+
if (hasOpeningTag(sourceWithoutStringLiterals, deprecatedComponent.componentName)) {
|
|
217
|
+
addDiagnostic(result, {
|
|
218
|
+
severity: "warning",
|
|
219
|
+
code: "deprecated_component",
|
|
220
|
+
message: deprecatedComponent.warning,
|
|
221
|
+
sourcePath: filePath,
|
|
222
|
+
componentName: deprecatedComponent.componentName,
|
|
223
|
+
suggestion: deprecatedComponent.suggestion
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const deprecatedProp of deprecatedAuthoringProps) {
|
|
228
|
+
if (hasOpeningTagWithProp(sourceWithoutStringLiterals, deprecatedProp.componentName, deprecatedProp.propName)) {
|
|
229
|
+
addDiagnostic(result, {
|
|
230
|
+
severity: "warning",
|
|
231
|
+
code: "deprecated_prop",
|
|
232
|
+
message: deprecatedProp.warning,
|
|
233
|
+
sourcePath: filePath,
|
|
234
|
+
componentName: deprecatedProp.componentName,
|
|
235
|
+
propName: deprecatedProp.propName,
|
|
236
|
+
suggestion: deprecatedProp.suggestion
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
function createValidationResult() {
|
|
243
|
+
return { diagnostics: [], errors: [], warnings: [] };
|
|
244
|
+
}
|
|
245
|
+
function addDiagnostic(result, diagnostic) {
|
|
246
|
+
result.diagnostics.push(diagnostic);
|
|
247
|
+
if (diagnostic.severity === "error") {
|
|
248
|
+
result.errors.push(diagnostic.message);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (diagnostic.severity === "warning") {
|
|
252
|
+
result.warnings.push(diagnostic.message);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export function formatValidationJson(result) {
|
|
256
|
+
return {
|
|
257
|
+
ok: result.errors.length === 0,
|
|
258
|
+
diagnostics: result.diagnostics
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
export function printValidationResult(result) {
|
|
262
|
+
for (const error of result.errors) {
|
|
263
|
+
console.error(`error: ${error}`);
|
|
264
|
+
}
|
|
265
|
+
for (const warning of result.warnings) {
|
|
266
|
+
console.warn(`warn: ${warning}`);
|
|
267
|
+
}
|
|
268
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
269
|
+
console.log("validate ok");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function stableIdExample(componentName) {
|
|
273
|
+
if (componentName.includes(".")) {
|
|
274
|
+
return `<${componentName} id="item.example" title="Example">Readable body.</${componentName}>`;
|
|
275
|
+
}
|
|
276
|
+
return `<${componentName} id="${componentName.toLowerCase()}.example" />`;
|
|
277
|
+
}
|
|
278
|
+
function stripStringLiterals(source) {
|
|
279
|
+
return source
|
|
280
|
+
.replace(/`(?:\\[\s\S]|[^`\\])*`/g, "``")
|
|
281
|
+
.replace(/"(?:\\.|[^"\\])*"/g, '""')
|
|
282
|
+
.replace(/'(?:\\.|[^'\\])*'/g, "''");
|
|
283
|
+
}
|
|
284
|
+
function hasOpeningTagWithoutProp(source, componentName, propName) {
|
|
285
|
+
const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
286
|
+
const tagPattern = new RegExp(`<${escapedName}(?=[\\s>/])[^>]*>`, "g");
|
|
287
|
+
const propPattern = new RegExp(`\\s${propName}\\s*=`);
|
|
288
|
+
for (const match of source.matchAll(tagPattern)) {
|
|
289
|
+
const openingTag = match[0];
|
|
290
|
+
if (!propPattern.test(openingTag)) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
function hasOpeningTag(source, componentName) {
|
|
297
|
+
const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
298
|
+
const tagPattern = new RegExp(`<${escapedName}(?=[\\s>/])`);
|
|
299
|
+
return tagPattern.test(source);
|
|
300
|
+
}
|
|
301
|
+
function hasOpeningTagWithProp(source, componentName, propName) {
|
|
302
|
+
const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
303
|
+
const tagPattern = new RegExp(`<${escapedName}(?=[\\s>/])[^>]*>`, "g");
|
|
304
|
+
const propPattern = new RegExp(`\\s${propName}\\s*=`);
|
|
305
|
+
for (const match of source.matchAll(tagPattern)) {
|
|
306
|
+
if (propPattern.test(match[0])) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
@@ -6,7 +6,8 @@ const defaultConfig = {
|
|
|
6
6
|
includeDefaultStyles: true,
|
|
7
7
|
outDir: "dist/artifacts",
|
|
8
8
|
port: 4321,
|
|
9
|
-
styles: []
|
|
9
|
+
styles: [],
|
|
10
|
+
tailwindSources: []
|
|
10
11
|
};
|
|
11
12
|
export async function loadConfig(projectRoot) {
|
|
12
13
|
const configPath = await findConfigPath(projectRoot);
|
|
@@ -22,7 +23,7 @@ export async function loadConfig(projectRoot) {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
async function findConfigPath(projectRoot) {
|
|
25
|
-
for (const filename of ["
|
|
26
|
+
for (const filename of ["mdx-artifacts.config.mjs", "mdx-artifacts.config.js", "mdx-artifacts.config.ts"]) {
|
|
26
27
|
const configPath = path.join(projectRoot, filename);
|
|
27
28
|
try {
|
|
28
29
|
await access(configPath);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InlineConfig, ViteDevServer } from "vite";
|
|
2
|
-
import { type ArtifactRoute } from "
|
|
3
|
-
import type {
|
|
2
|
+
import { type ArtifactRoute } from "../state/artifact-state";
|
|
3
|
+
import type { MdxArtifactsConfig } from "../config/types";
|
|
4
4
|
export type ArtifactProject = {
|
|
5
5
|
artifact: ArtifactRoute;
|
|
6
6
|
tmpDir: string;
|
|
@@ -8,6 +8,6 @@ export type ArtifactProject = {
|
|
|
8
8
|
config: InlineConfig;
|
|
9
9
|
cleanup: () => Promise<void>;
|
|
10
10
|
};
|
|
11
|
-
export declare function createArtifactProject(projectRoot: string, mdxPath: string, config: Required<
|
|
11
|
+
export declare function createArtifactProject(projectRoot: string, mdxPath: string, config: Required<MdxArtifactsConfig>): Promise<ArtifactProject>;
|
|
12
12
|
export declare function startDevServer(project: ArtifactProject): Promise<ViteDevServer>;
|
|
13
13
|
export declare function buildArtifact(project: ArtifactProject): Promise<string>;
|
|
@@ -7,21 +7,24 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { createServer, build as viteBuild } from "vite";
|
|
10
|
-
import { createArtifactMeta, createArtifactRoute, readArtifactState, writeArtifactState } from "
|
|
10
|
+
import { createArtifactMeta, createArtifactRoute, readArtifactState, writeArtifactState } from "../state/artifact-state.js";
|
|
11
|
+
import { addInteractionItemService, promoteInteractionService, removeInteractionItemService, resetInteractionService, setInteractionOrderService, updateInteractionItemService } from "../services/interaction-service.js";
|
|
11
12
|
const packageCliDir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const defaultStylesPath = path.resolve(packageCliDir, "
|
|
13
|
+
const defaultStylesPath = path.resolve(packageCliDir, "../../react/styles.css");
|
|
13
14
|
export async function createArtifactProject(projectRoot, mdxPath, config) {
|
|
14
|
-
const tmpDir = path.join(projectRoot, ".
|
|
15
|
+
const tmpDir = path.join(projectRoot, ".mdx-artifacts", "tmp", randomUUID());
|
|
15
16
|
const artifact = createArtifactRoute(projectRoot, mdxPath, config.docsDir);
|
|
16
17
|
const srcDir = path.join(tmpDir, "src");
|
|
17
18
|
const distDir = path.join(tmpDir, "dist");
|
|
18
19
|
const entryPath = path.join(srcDir, "entry.tsx");
|
|
20
|
+
const tailwindSourcePath = path.join(srcDir, "artifact-tailwind-sources.css");
|
|
19
21
|
const mdxImport = toRelativeImport(entryPath, mdxPath);
|
|
20
|
-
const styleImports = createStyleImports(projectRoot, entryPath, config);
|
|
22
|
+
const styleImports = createStyleImports(projectRoot, entryPath, config, tailwindSourcePath);
|
|
21
23
|
const reactEntryPath = await resolveReactEntryPath();
|
|
22
24
|
const reactEntryImport = toRelativeImport(entryPath, reactEntryPath);
|
|
23
25
|
const reactAliases = resolveReactAliases(projectRoot);
|
|
24
26
|
await mkdir(srcDir, { recursive: true });
|
|
27
|
+
await writeFile(tailwindSourcePath, createTailwindSourceCss(projectRoot, tailwindSourcePath, mdxPath, config));
|
|
25
28
|
await writeFile(path.join(tmpDir, "index.html"), `<!doctype html>
|
|
26
29
|
<html lang="en">
|
|
27
30
|
<head>
|
|
@@ -37,7 +40,7 @@ export async function createArtifactProject(projectRoot, mdxPath, config) {
|
|
|
37
40
|
`);
|
|
38
41
|
await writeFile(entryPath, `import React from "react";
|
|
39
42
|
import { createRoot } from "react-dom/client";
|
|
40
|
-
import { ArtifactStateProvider, CommentLayer } from "${reactEntryImport}";
|
|
43
|
+
import { ArtifactStateProvider, CommentLayer, artifactMdxComponents } from "${reactEntryImport}";
|
|
41
44
|
import Doc from "${mdxImport}";
|
|
42
45
|
${styleImports}
|
|
43
46
|
|
|
@@ -47,7 +50,7 @@ function App() {
|
|
|
47
50
|
<article className="ak-document">
|
|
48
51
|
<ArtifactStateProvider>
|
|
49
52
|
<CommentLayer>
|
|
50
|
-
<Doc />
|
|
53
|
+
<Doc components={artifactMdxComponents} />
|
|
51
54
|
</CommentLayer>
|
|
52
55
|
</ArtifactStateProvider>
|
|
53
56
|
</article>
|
|
@@ -98,7 +101,7 @@ createRoot(document.getElementById("root")!).render(<App />);
|
|
|
98
101
|
}
|
|
99
102
|
function artifactStatePlugin(projectRoot, artifact) {
|
|
100
103
|
return {
|
|
101
|
-
name: "
|
|
104
|
+
name: "mdx-artifacts-state",
|
|
102
105
|
configureServer(server) {
|
|
103
106
|
server.middlewares.use(async (request, response, next) => {
|
|
104
107
|
const requestUrl = new URL(request.url ?? "/", "http://localhost");
|
|
@@ -111,6 +114,10 @@ function artifactStatePlugin(projectRoot, artifact) {
|
|
|
111
114
|
await handleArtifactState(projectRoot, artifact, request, response);
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
117
|
+
if (requestUrl.pathname.startsWith("/__artifact/interactions/")) {
|
|
118
|
+
await handleArtifactInteraction(projectRoot, artifact, requestUrl.pathname, request, response);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
114
121
|
}
|
|
115
122
|
catch (error) {
|
|
116
123
|
sendJson(response, 500, {
|
|
@@ -150,6 +157,143 @@ async function handleArtifactState(projectRoot, artifact, request, response) {
|
|
|
150
157
|
}
|
|
151
158
|
sendJson(response, 405, { error: "Method not allowed." });
|
|
152
159
|
}
|
|
160
|
+
async function handleArtifactInteraction(projectRoot, artifact, pathname, request, response) {
|
|
161
|
+
if (request.method !== "POST") {
|
|
162
|
+
sendJson(response, 405, { error: "Method not allowed." });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let value;
|
|
166
|
+
try {
|
|
167
|
+
value = JSON.parse(await readRequestBody(request));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
sendJson(response, 400, { error: "Request body must be valid JSON." });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
if (pathname === "/__artifact/interactions/set-order") {
|
|
175
|
+
const body = parseSetOrderRequest(value);
|
|
176
|
+
const result = await setInteractionOrderService(projectRoot, artifact.sourceRelativePath, body.id, body.orderedIds);
|
|
177
|
+
sendJson(response, 200, { ok: true, result, state: result.state });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (pathname === "/__artifact/interactions/reset") {
|
|
181
|
+
const body = parseInteractionIdRequest(value, "reset");
|
|
182
|
+
const result = await resetInteractionService(projectRoot, artifact.sourceRelativePath, body.id);
|
|
183
|
+
sendJson(response, 200, { ok: true, result, state: result.state });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (pathname === "/__artifact/interactions/promote") {
|
|
187
|
+
const body = parseInteractionIdRequest(value, "promote");
|
|
188
|
+
const result = await promoteInteractionService(projectRoot, artifact.sourceRelativePath, body.id);
|
|
189
|
+
sendJson(response, 200, { ok: true, result, state: result.state });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (pathname === "/__artifact/interactions/add-item") {
|
|
193
|
+
const body = parseAddItemRequest(value);
|
|
194
|
+
const result = await addInteractionItemService(projectRoot, artifact.sourceRelativePath, body.id, body.item, {
|
|
195
|
+
afterId: body.afterId
|
|
196
|
+
});
|
|
197
|
+
sendJson(response, 200, { ok: true, result, state: result.state });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (pathname === "/__artifact/interactions/remove-item") {
|
|
201
|
+
const body = parseItemIdRequest(value, "remove-item");
|
|
202
|
+
const result = await removeInteractionItemService(projectRoot, artifact.sourceRelativePath, body.id, body.itemId);
|
|
203
|
+
sendJson(response, 200, { ok: true, result, state: result.state });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (pathname === "/__artifact/interactions/update-item") {
|
|
207
|
+
const body = parseUpdateItemRequest(value);
|
|
208
|
+
const result = await updateInteractionItemService(projectRoot, artifact.sourceRelativePath, body.id, body.itemId, body.patch);
|
|
209
|
+
sendJson(response, 200, { ok: true, result, state: result.state });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
sendJson(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
sendJson(response, 404, { error: "Interaction endpoint not found." });
|
|
218
|
+
}
|
|
219
|
+
function parseAddItemRequest(value) {
|
|
220
|
+
const body = parseInteractionIdRequest(value, "add-item");
|
|
221
|
+
if (!isRecord(value) || !isRecord(value.item)) {
|
|
222
|
+
throw new Error("interactions add-item requires item.");
|
|
223
|
+
}
|
|
224
|
+
if (typeof value.item.id !== "string" || !value.item.id) {
|
|
225
|
+
throw new Error("interactions add-item requires item.id.");
|
|
226
|
+
}
|
|
227
|
+
if (typeof value.item.title !== "string" || !value.item.title) {
|
|
228
|
+
throw new Error("interactions add-item requires item.title.");
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
id: body.id,
|
|
232
|
+
item: normalizeItem(value.item),
|
|
233
|
+
afterId: typeof value.afterId === "string" && value.afterId ? value.afterId : undefined
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function parseUpdateItemRequest(value) {
|
|
237
|
+
const body = parseItemIdRequest(value, "update-item");
|
|
238
|
+
if (!isRecord(value) || !isRecord(value.patch)) {
|
|
239
|
+
throw new Error("interactions update-item requires patch.");
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
id: body.id,
|
|
243
|
+
itemId: body.itemId,
|
|
244
|
+
patch: normalizeItemPatch(value.patch)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function parseItemIdRequest(value, action) {
|
|
248
|
+
const body = parseInteractionIdRequest(value, action);
|
|
249
|
+
if (!isRecord(value) || typeof value.itemId !== "string" || !value.itemId) {
|
|
250
|
+
throw new Error(`interactions ${action} requires itemId.`);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
id: body.id,
|
|
254
|
+
itemId: value.itemId
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function parseSetOrderRequest(value) {
|
|
258
|
+
const body = parseInteractionIdRequest(value, "set-order");
|
|
259
|
+
if (!isRecord(value) || !Array.isArray(value.orderedIds)) {
|
|
260
|
+
throw new Error("interactions set-order requires orderedIds.");
|
|
261
|
+
}
|
|
262
|
+
const orderedIds = value.orderedIds.filter((itemId) => typeof itemId === "string");
|
|
263
|
+
if (orderedIds.length !== value.orderedIds.length || orderedIds.length === 0) {
|
|
264
|
+
throw new Error("interactions set-order requires non-empty string orderedIds.");
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
id: body.id,
|
|
268
|
+
orderedIds
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function normalizeItem(value) {
|
|
272
|
+
return {
|
|
273
|
+
id: value.id,
|
|
274
|
+
title: value.title,
|
|
275
|
+
...normalizeItemPatch(value)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function normalizeItemPatch(value) {
|
|
279
|
+
return {
|
|
280
|
+
...(typeof value.title === "string" ? { title: value.title } : {}),
|
|
281
|
+
...("summary" in value ? { summary: typeof value.summary === "string" ? value.summary : undefined } : {}),
|
|
282
|
+
...("badge" in value ? { badge: typeof value.badge === "string" ? value.badge : undefined } : {}),
|
|
283
|
+
...("tags" in value
|
|
284
|
+
? { tags: Array.isArray(value.tags) ? value.tags.filter((tag) => typeof tag === "string") : undefined }
|
|
285
|
+
: {}),
|
|
286
|
+
...(typeof value.disabled === "boolean" ? { disabled: value.disabled } : {})
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function parseInteractionIdRequest(value, action) {
|
|
290
|
+
if (!isRecord(value) || typeof value.id !== "string" || !value.id) {
|
|
291
|
+
throw new Error(`interactions ${action} requires id.`);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
id: value.id
|
|
295
|
+
};
|
|
296
|
+
}
|
|
153
297
|
function readRequestBody(request) {
|
|
154
298
|
return new Promise((resolve, reject) => {
|
|
155
299
|
let body = "";
|
|
@@ -166,9 +310,12 @@ function sendJson(response, statusCode, value) {
|
|
|
166
310
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
167
311
|
response.end(JSON.stringify(value, null, 2));
|
|
168
312
|
}
|
|
313
|
+
function isRecord(value) {
|
|
314
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
315
|
+
}
|
|
169
316
|
async function resolveReactEntryPath() {
|
|
170
|
-
const builtEntryPath = path.resolve(packageCliDir, "
|
|
171
|
-
const sourceEntryPath = path.resolve(packageCliDir, "
|
|
317
|
+
const builtEntryPath = path.resolve(packageCliDir, "../../react/index.js");
|
|
318
|
+
const sourceEntryPath = path.resolve(packageCliDir, "../../react/index.ts");
|
|
172
319
|
try {
|
|
173
320
|
await access(builtEntryPath);
|
|
174
321
|
return builtEntryPath;
|
|
@@ -234,13 +381,26 @@ function toRelativeImport(fromFile, targetFile) {
|
|
|
234
381
|
const relative = path.relative(path.dirname(fromFile), targetFile).replaceAll(path.sep, "/");
|
|
235
382
|
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
236
383
|
}
|
|
237
|
-
function createStyleImports(projectRoot, entryPath, config) {
|
|
384
|
+
function createStyleImports(projectRoot, entryPath, config, tailwindSourcePath) {
|
|
238
385
|
const styles = [
|
|
386
|
+
tailwindSourcePath,
|
|
239
387
|
...(config.includeDefaultStyles ? [defaultStylesPath] : []),
|
|
240
388
|
...config.styles.map((stylePath) => path.resolve(projectRoot, stylePath))
|
|
241
389
|
];
|
|
242
390
|
return styles.map((stylePath) => `import "${toRelativeImport(entryPath, stylePath)}";`).join("\n");
|
|
243
391
|
}
|
|
392
|
+
function createTailwindSourceCss(projectRoot, sourceStylesPath, mdxPath, config) {
|
|
393
|
+
const sources = [
|
|
394
|
+
mdxPath,
|
|
395
|
+
...config.tailwindSources.map((sourcePath) => path.resolve(projectRoot, sourcePath))
|
|
396
|
+
];
|
|
397
|
+
return `@import "tailwindcss";\n${sources
|
|
398
|
+
.map((sourcePath) => `@source "${escapeCssString(toRelativeImport(sourceStylesPath, sourcePath))}";`)
|
|
399
|
+
.join("\n")}\n`;
|
|
400
|
+
}
|
|
401
|
+
function escapeCssString(value) {
|
|
402
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
403
|
+
}
|
|
244
404
|
async function replaceAsync(source, pattern, replacer) {
|
|
245
405
|
const matches = Array.from(source.matchAll(pattern));
|
|
246
406
|
let output = source;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|