executable-stories-demo 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.
- package/README.md +132 -0
- package/bin/executable-stories-demo.js +2 -0
- package/bin/intent.js +3 -0
- package/dist/cli.js +834 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +727 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/templates/astro-demo-starlight/astro.config.mjs +26 -0
- package/templates/astro-demo-starlight/package.json +26 -0
- package/templates/astro-demo-starlight/src/content/docs/index.mdx +23 -0
- package/templates/astro-demo-starlight/src/content/docs/themes.mdx +190 -0
- package/templates/astro-demo-starlight/src/content.config.ts +7 -0
- package/templates/astro-demo-starlight/src/styles/global.css +588 -0
- package/templates/astro-demo-starlight/src/styles/themes/corporate.css +159 -0
- package/templates/astro-demo-starlight/src/styles/themes/dashboard.css +241 -0
- package/templates/astro-demo-starlight/src/styles/themes/default.css +106 -0
- package/templates/astro-demo-starlight/src/styles/themes/minimal.css +212 -0
- package/templates/astro-demo-starlight/src/styles/themes/playful.css +263 -0
- package/templates/astro-demo-starlight/src/styles/themes/terminal.css +218 -0
- package/templates/astro-demo-starlight/tsconfig.json +5 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import {
|
|
7
|
+
ReportGenerator,
|
|
8
|
+
canonicalizeRun,
|
|
9
|
+
copyMarkdownAssets
|
|
10
|
+
} from "executable-stories-formatters";
|
|
11
|
+
var DEFAULT_CONFIG = {
|
|
12
|
+
productName: "Product Demo",
|
|
13
|
+
tagline: "Executable stories turned into product walkthroughs.",
|
|
14
|
+
theme: "default",
|
|
15
|
+
template: "splash",
|
|
16
|
+
cta: {
|
|
17
|
+
primary: "Get Started",
|
|
18
|
+
url: "/"
|
|
19
|
+
},
|
|
20
|
+
scenarios: {
|
|
21
|
+
order: []
|
|
22
|
+
},
|
|
23
|
+
// Splash audiences = customers/prospects; capability framing reads better than test counts.
|
|
24
|
+
// Dashboard mode flips to "test" by default in loadConfig.
|
|
25
|
+
stats: {
|
|
26
|
+
mode: "capability"
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var SUPPORTED_THEMES = /* @__PURE__ */ new Set([
|
|
30
|
+
"default",
|
|
31
|
+
"corporate",
|
|
32
|
+
"terminal",
|
|
33
|
+
"minimal",
|
|
34
|
+
"dashboard",
|
|
35
|
+
"playful"
|
|
36
|
+
]);
|
|
37
|
+
var TEST_EXTENSIONS = [
|
|
38
|
+
".test.ts",
|
|
39
|
+
".test.tsx",
|
|
40
|
+
".spec.ts",
|
|
41
|
+
".spec.tsx",
|
|
42
|
+
".test.js",
|
|
43
|
+
".spec.js",
|
|
44
|
+
".story.test.ts",
|
|
45
|
+
".story.spec.ts"
|
|
46
|
+
];
|
|
47
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
function initDemo(options = {}) {
|
|
49
|
+
const targetDir = path.resolve(options.targetDir ?? "./demo-site");
|
|
50
|
+
const force = options.force ?? false;
|
|
51
|
+
if (fs.existsSync(targetDir)) {
|
|
52
|
+
const entries = fs.readdirSync(targetDir);
|
|
53
|
+
if (entries.length > 0 && !force) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const templateDir = path.resolve(
|
|
60
|
+
__dirname,
|
|
61
|
+
"..",
|
|
62
|
+
"templates",
|
|
63
|
+
"astro-demo-starlight"
|
|
64
|
+
);
|
|
65
|
+
if (!fs.existsSync(templateDir)) {
|
|
66
|
+
throw new Error(`Template directory not found at ${templateDir}`);
|
|
67
|
+
}
|
|
68
|
+
copyDirRecursive(templateDir, targetDir);
|
|
69
|
+
const productName = options.productName ?? toTitleCase(path.basename(targetDir));
|
|
70
|
+
const config = {
|
|
71
|
+
productName,
|
|
72
|
+
tagline: DEFAULT_CONFIG.tagline,
|
|
73
|
+
theme: DEFAULT_CONFIG.theme,
|
|
74
|
+
template: DEFAULT_CONFIG.template,
|
|
75
|
+
cta: { ...DEFAULT_CONFIG.cta },
|
|
76
|
+
scenarios: { order: [] },
|
|
77
|
+
stats: { mode: DEFAULT_CONFIG.stats.mode },
|
|
78
|
+
seo: {
|
|
79
|
+
title: productName,
|
|
80
|
+
description: DEFAULT_CONFIG.tagline
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const configPath = path.join(targetDir, "demo.config.json");
|
|
84
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
85
|
+
`, "utf8");
|
|
86
|
+
return { targetDir, configPath };
|
|
87
|
+
}
|
|
88
|
+
async function buildDemo(options) {
|
|
89
|
+
const siteDir = path.resolve(options.siteDir);
|
|
90
|
+
const inputPath = path.resolve(options.input);
|
|
91
|
+
const configPath = path.resolve(options.configPath ?? path.join(siteDir, "demo.config.json"));
|
|
92
|
+
const astroConfigPath = path.join(siteDir, "astro.config.mjs");
|
|
93
|
+
if (!fs.existsSync(siteDir)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Demo site directory not found: ${siteDir}. Run "executable-stories-demo init <dir>" first.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (!fs.existsSync(astroConfigPath)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`astro.config.mjs not found in ${siteDir}. This directory is not a valid demo site scaffold.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (!fs.existsSync(inputPath)) {
|
|
104
|
+
throw new Error(`Input run file not found: ${inputPath}`);
|
|
105
|
+
}
|
|
106
|
+
const config = loadConfig(configPath);
|
|
107
|
+
const run = loadRun(inputPath);
|
|
108
|
+
const docsDir = path.join(siteDir, "src", "content", "docs");
|
|
109
|
+
const storiesDir = path.join(docsDir, "stories");
|
|
110
|
+
const strict = options.strict ?? false;
|
|
111
|
+
const allowMissingAssets = strict ? false : options.allowMissingAssets ?? true;
|
|
112
|
+
const assetsDir = resolveAssetsDir(siteDir, options.assetsDir);
|
|
113
|
+
const assetsBaseUrl = normalizeAssetsBaseUrl(options.assetsBaseUrl);
|
|
114
|
+
fs.mkdirSync(storiesDir, { recursive: true });
|
|
115
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
116
|
+
const generator = new ReportGenerator({
|
|
117
|
+
formats: ["astro"],
|
|
118
|
+
outputDir: storiesDir,
|
|
119
|
+
output: {
|
|
120
|
+
mode: "colocated",
|
|
121
|
+
colocatedStyle: "mirrored"
|
|
122
|
+
},
|
|
123
|
+
astro: {
|
|
124
|
+
assetsDir,
|
|
125
|
+
assetsBaseUrl,
|
|
126
|
+
markdown: {
|
|
127
|
+
title: `${config.productName} Stories`
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
assetMode: "copy",
|
|
131
|
+
allowMissingAssets
|
|
132
|
+
});
|
|
133
|
+
const output = await generator.generate(run);
|
|
134
|
+
const astroFiles = output.get("astro") ?? [];
|
|
135
|
+
appendAttachmentsToPages({
|
|
136
|
+
astroFiles,
|
|
137
|
+
run,
|
|
138
|
+
storiesDir,
|
|
139
|
+
assetsDir,
|
|
140
|
+
assetsBaseUrl,
|
|
141
|
+
allowMissingAssets
|
|
142
|
+
});
|
|
143
|
+
const pages = toPages(astroFiles, docsDir, config.scenarios.order);
|
|
144
|
+
writeLandingPage(path.join(docsDir, "index.mdx"), config, pages, run, docsDir, assetsBaseUrl);
|
|
145
|
+
applyThemeToAstroConfig(siteDir, config.theme);
|
|
146
|
+
const manifest = {
|
|
147
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
148
|
+
input: toPosix(inputPath),
|
|
149
|
+
config: toPosix(configPath),
|
|
150
|
+
productName: config.productName,
|
|
151
|
+
theme: config.theme,
|
|
152
|
+
assets: {
|
|
153
|
+
dir: toPosix(assetsDir),
|
|
154
|
+
baseUrl: assetsBaseUrl,
|
|
155
|
+
allowMissing: allowMissingAssets,
|
|
156
|
+
strict
|
|
157
|
+
},
|
|
158
|
+
stats: {
|
|
159
|
+
scenarios: run.testCases.length,
|
|
160
|
+
passed: run.testCases.filter((tc) => tc.status === "passed").length,
|
|
161
|
+
failed: run.testCases.filter((tc) => tc.status === "failed").length,
|
|
162
|
+
skipped: run.testCases.filter((tc) => tc.status === "skipped").length,
|
|
163
|
+
pending: run.testCases.filter((tc) => tc.status === "pending").length
|
|
164
|
+
},
|
|
165
|
+
pages
|
|
166
|
+
};
|
|
167
|
+
const manifestPath = path.join(siteDir, "demo-manifest.json");
|
|
168
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
169
|
+
`, "utf8");
|
|
170
|
+
return {
|
|
171
|
+
pages,
|
|
172
|
+
manifestPath,
|
|
173
|
+
storiesDir
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function previewDemo(options) {
|
|
177
|
+
const siteDir = path.resolve(options.siteDir);
|
|
178
|
+
const mode = options.mode ?? "dev";
|
|
179
|
+
const command = mode === "build" ? "build" : mode === "preview" ? "preview" : "dev";
|
|
180
|
+
const result = spawnSync("pnpm", [command], {
|
|
181
|
+
cwd: siteDir,
|
|
182
|
+
stdio: "inherit",
|
|
183
|
+
shell: process.platform === "win32"
|
|
184
|
+
});
|
|
185
|
+
if (result.status !== 0) {
|
|
186
|
+
throw new Error(`pnpm ${command} failed with exit code ${result.status ?? 1}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function loadConfig(configPath) {
|
|
190
|
+
const userConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
|
191
|
+
const theme = normalizeTheme(userConfig.theme);
|
|
192
|
+
const template = userConfig.template === "dashboard" ? "dashboard" : DEFAULT_CONFIG.template;
|
|
193
|
+
const statsMode = (() => {
|
|
194
|
+
const m = userConfig.stats?.mode;
|
|
195
|
+
if (m === "test" || m === "capability" || m === "off") return m;
|
|
196
|
+
return template === "dashboard" ? "test" : DEFAULT_CONFIG.stats.mode;
|
|
197
|
+
})();
|
|
198
|
+
return {
|
|
199
|
+
productName: userConfig.productName ?? DEFAULT_CONFIG.productName,
|
|
200
|
+
tagline: userConfig.tagline ?? DEFAULT_CONFIG.tagline,
|
|
201
|
+
theme,
|
|
202
|
+
template,
|
|
203
|
+
cta: {
|
|
204
|
+
primary: userConfig.cta?.primary ?? DEFAULT_CONFIG.cta.primary,
|
|
205
|
+
url: userConfig.cta?.url ?? DEFAULT_CONFIG.cta.url
|
|
206
|
+
},
|
|
207
|
+
scenarios: {
|
|
208
|
+
order: userConfig.scenarios?.order ?? [...DEFAULT_CONFIG.scenarios.order]
|
|
209
|
+
},
|
|
210
|
+
stats: { mode: statsMode },
|
|
211
|
+
featured: {
|
|
212
|
+
scenario: userConfig.featured?.scenario
|
|
213
|
+
},
|
|
214
|
+
branding: {
|
|
215
|
+
logo: userConfig.branding?.logo,
|
|
216
|
+
ogImage: userConfig.branding?.ogImage,
|
|
217
|
+
favicon: userConfig.branding?.favicon,
|
|
218
|
+
accent: userConfig.branding?.accent
|
|
219
|
+
},
|
|
220
|
+
seo: {
|
|
221
|
+
title: userConfig.seo?.title,
|
|
222
|
+
description: userConfig.seo?.description,
|
|
223
|
+
twitter: userConfig.seo?.twitter,
|
|
224
|
+
canonical: userConfig.seo?.canonical
|
|
225
|
+
},
|
|
226
|
+
sections: userConfig.sections ?? []
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function loadRun(inputPath) {
|
|
230
|
+
const payload = JSON.parse(fs.readFileSync(inputPath, "utf8"));
|
|
231
|
+
if (isRawLikePayload(payload)) {
|
|
232
|
+
return canonicalizeRun(payload);
|
|
233
|
+
}
|
|
234
|
+
return payload;
|
|
235
|
+
}
|
|
236
|
+
function isRawLikePayload(payload) {
|
|
237
|
+
if (!payload || typeof payload !== "object") return false;
|
|
238
|
+
const maybeCases = payload.testCases;
|
|
239
|
+
const firstStatus = maybeCases?.[0]?.status;
|
|
240
|
+
return firstStatus === "pass" || firstStatus === "fail" || firstStatus === "skip";
|
|
241
|
+
}
|
|
242
|
+
function appendAttachmentsToPages(args) {
|
|
243
|
+
const byPage = groupAttachmentsByPage(args.run.testCases, args.storiesDir, "index");
|
|
244
|
+
for (const filePath of args.astroFiles) {
|
|
245
|
+
const attachments = byPage.get(toPosix(path.resolve(filePath)));
|
|
246
|
+
if (!attachments || attachments.length === 0) continue;
|
|
247
|
+
const markdownDir = path.dirname(filePath);
|
|
248
|
+
const unique = dedupeAttachments(attachments);
|
|
249
|
+
const rendered = renderAttachmentSection(unique, markdownDir, args.run.projectRoot);
|
|
250
|
+
if (rendered.length === 0) continue;
|
|
251
|
+
const original = fs.readFileSync(filePath, "utf8");
|
|
252
|
+
const appended = `${original.trimEnd()}
|
|
253
|
+
|
|
254
|
+
${rendered}
|
|
255
|
+
`;
|
|
256
|
+
const copied = copyMarkdownAssets({
|
|
257
|
+
markdown: appended,
|
|
258
|
+
markdownDir,
|
|
259
|
+
assetsDir: args.assetsDir,
|
|
260
|
+
assetsBaseUrl: args.assetsBaseUrl,
|
|
261
|
+
allowMissing: args.allowMissingAssets
|
|
262
|
+
});
|
|
263
|
+
fs.writeFileSync(filePath, copied.markdown, "utf8");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function groupAttachmentsByPage(testCases, storiesDir, outputName) {
|
|
267
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
268
|
+
const baseDir = toPosix(path.resolve(storiesDir));
|
|
269
|
+
for (const tc of testCases) {
|
|
270
|
+
if (tc.attachments.length === 0) continue;
|
|
271
|
+
const outputPath = computeStoryOutputPath(tc.sourceFile, baseDir, outputName);
|
|
272
|
+
const existing = grouped.get(outputPath) ?? [];
|
|
273
|
+
existing.push(...tc.attachments);
|
|
274
|
+
grouped.set(outputPath, existing);
|
|
275
|
+
}
|
|
276
|
+
return grouped;
|
|
277
|
+
}
|
|
278
|
+
function computeStoryOutputPath(sourceFile, baseOutputDir, outputName) {
|
|
279
|
+
if (sourceFile === "unknown") {
|
|
280
|
+
return toPosix(path.join(baseOutputDir, `${outputName}.md`));
|
|
281
|
+
}
|
|
282
|
+
const normalizedSource = toPosix(sourceFile);
|
|
283
|
+
const dirOfSource = path.posix.dirname(normalizedSource);
|
|
284
|
+
let baseName = path.posix.basename(normalizedSource);
|
|
285
|
+
for (const extension of TEST_EXTENSIONS) {
|
|
286
|
+
if (baseName.endsWith(extension)) {
|
|
287
|
+
baseName = baseName.slice(0, -extension.length);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const fileName = `${baseName}.${outputName}.md`;
|
|
292
|
+
return toPosix(path.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
293
|
+
}
|
|
294
|
+
function dedupeAttachments(attachments) {
|
|
295
|
+
const seen = /* @__PURE__ */ new Set();
|
|
296
|
+
const deduped = [];
|
|
297
|
+
for (const attachment of attachments) {
|
|
298
|
+
const key = `${attachment.name}|${attachment.mediaType}|${attachment.contentEncoding}|${attachment.body}`;
|
|
299
|
+
if (seen.has(key)) continue;
|
|
300
|
+
seen.add(key);
|
|
301
|
+
deduped.push(attachment);
|
|
302
|
+
}
|
|
303
|
+
return deduped;
|
|
304
|
+
}
|
|
305
|
+
function renderAttachmentSection(attachments, markdownDir, projectRoot) {
|
|
306
|
+
const lines = ["## Media", ""];
|
|
307
|
+
for (const attachment of attachments) {
|
|
308
|
+
const source = resolveAttachmentSource(attachment, markdownDir, projectRoot);
|
|
309
|
+
if (!source) continue;
|
|
310
|
+
const label = attachment.name || "Attachment";
|
|
311
|
+
if (attachment.mediaType.startsWith("video/")) {
|
|
312
|
+
lines.push(`### ${label}`);
|
|
313
|
+
lines.push("");
|
|
314
|
+
lines.push(`<video controls preload="metadata" src="${source}"></video>`);
|
|
315
|
+
lines.push("");
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (attachment.mediaType.startsWith("image/")) {
|
|
319
|
+
lines.push(`### ${label}`);
|
|
320
|
+
lines.push("");
|
|
321
|
+
lines.push(``);
|
|
322
|
+
lines.push("");
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
lines.push(`- [${label}](${source})`);
|
|
326
|
+
}
|
|
327
|
+
if (lines.length <= 2) return "";
|
|
328
|
+
return lines.join("\n");
|
|
329
|
+
}
|
|
330
|
+
function resolveAttachmentSource(attachment, markdownDir, projectRoot) {
|
|
331
|
+
if (!attachment.body) return void 0;
|
|
332
|
+
if (attachment.contentEncoding === "BASE64") {
|
|
333
|
+
return `data:${attachment.mediaType};base64,${attachment.body}`;
|
|
334
|
+
}
|
|
335
|
+
const body = attachment.body.trim();
|
|
336
|
+
if (body.startsWith("http://") || body.startsWith("https://") || body.startsWith("data:") || body.startsWith("#")) {
|
|
337
|
+
return body;
|
|
338
|
+
}
|
|
339
|
+
if (path.isAbsolute(body) && fs.existsSync(body)) {
|
|
340
|
+
return toPosix(path.relative(markdownDir, body));
|
|
341
|
+
}
|
|
342
|
+
const candidateFromProject = path.resolve(projectRoot, body);
|
|
343
|
+
if (fs.existsSync(candidateFromProject)) {
|
|
344
|
+
return toPosix(path.relative(markdownDir, candidateFromProject));
|
|
345
|
+
}
|
|
346
|
+
return body;
|
|
347
|
+
}
|
|
348
|
+
function toPages(astroFiles, docsDir, orderedSlugs) {
|
|
349
|
+
const pages = astroFiles.map((absPath) => {
|
|
350
|
+
const rel = toPosix(path.relative(docsDir, absPath));
|
|
351
|
+
const withoutExt = rel.replace(/\.md$/, "");
|
|
352
|
+
const slug = withoutExt;
|
|
353
|
+
const title = toTitleCase(normalizePageName(path.basename(withoutExt)));
|
|
354
|
+
return {
|
|
355
|
+
title,
|
|
356
|
+
slug,
|
|
357
|
+
file: rel
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
if (orderedSlugs.length === 0) return pages;
|
|
361
|
+
const rank = new Map(orderedSlugs.map((slug, index) => [slug, index]));
|
|
362
|
+
return [...pages].sort((a, b) => {
|
|
363
|
+
const ar = rank.get(a.slug);
|
|
364
|
+
const br = rank.get(b.slug);
|
|
365
|
+
if (ar === void 0 && br === void 0) return a.slug.localeCompare(b.slug);
|
|
366
|
+
if (ar === void 0) return 1;
|
|
367
|
+
if (br === void 0) return -1;
|
|
368
|
+
return ar - br;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function normalizePageName(fileBase) {
|
|
372
|
+
return fileBase.replace(/\.story\.index$/i, "").replace(/\.index$/i, "").replace(/\.story$/i, "");
|
|
373
|
+
}
|
|
374
|
+
function applyThemeToAstroConfig(siteDir, requestedTheme) {
|
|
375
|
+
const configPath = path.join(siteDir, "astro.config.mjs");
|
|
376
|
+
if (!fs.existsSync(configPath)) return;
|
|
377
|
+
const theme = normalizeTheme(requestedTheme);
|
|
378
|
+
const config = fs.readFileSync(configPath, "utf8");
|
|
379
|
+
const updated = config.replace(
|
|
380
|
+
/'\.\/src\/styles\/themes\/[^']+\.css'/,
|
|
381
|
+
`'./src/styles/themes/${theme}.css'`
|
|
382
|
+
);
|
|
383
|
+
fs.writeFileSync(configPath, updated, "utf8");
|
|
384
|
+
}
|
|
385
|
+
function normalizeTheme(requestedTheme) {
|
|
386
|
+
if (!requestedTheme) return DEFAULT_CONFIG.theme;
|
|
387
|
+
return SUPPORTED_THEMES.has(requestedTheme) ? requestedTheme : DEFAULT_CONFIG.theme;
|
|
388
|
+
}
|
|
389
|
+
function computeStats(run) {
|
|
390
|
+
return {
|
|
391
|
+
total: run.testCases.length,
|
|
392
|
+
passed: run.testCases.filter((tc) => tc.status === "passed").length,
|
|
393
|
+
failed: run.testCases.filter((tc) => tc.status === "failed").length,
|
|
394
|
+
skipped: run.testCases.filter((tc) => tc.status === "skipped").length
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function renderHead(config) {
|
|
398
|
+
const seoTitle = config.seo.title ?? config.productName;
|
|
399
|
+
const seoDesc = config.seo.description ?? config.tagline;
|
|
400
|
+
const ogImage = config.branding.ogImage;
|
|
401
|
+
const canonical = config.seo.canonical;
|
|
402
|
+
const twitter = config.seo.twitter;
|
|
403
|
+
const tags = [
|
|
404
|
+
{ tag: "meta", attrs: { property: "og:type", content: "website" } },
|
|
405
|
+
{ tag: "meta", attrs: { property: "og:title", content: seoTitle } },
|
|
406
|
+
{ tag: "meta", attrs: { property: "og:description", content: seoDesc } },
|
|
407
|
+
{ tag: "meta", attrs: { name: "twitter:card", content: ogImage ? "summary_large_image" : "summary" } },
|
|
408
|
+
{ tag: "meta", attrs: { name: "twitter:title", content: seoTitle } },
|
|
409
|
+
{ tag: "meta", attrs: { name: "twitter:description", content: seoDesc } }
|
|
410
|
+
];
|
|
411
|
+
if (ogImage) {
|
|
412
|
+
tags.push({ tag: "meta", attrs: { property: "og:image", content: ogImage } });
|
|
413
|
+
tags.push({ tag: "meta", attrs: { name: "twitter:image", content: ogImage } });
|
|
414
|
+
}
|
|
415
|
+
if (canonical) {
|
|
416
|
+
tags.push({ tag: "meta", attrs: { property: "og:url", content: canonical } });
|
|
417
|
+
tags.push({ tag: "link", attrs: { rel: "canonical", href: canonical } });
|
|
418
|
+
}
|
|
419
|
+
if (twitter) {
|
|
420
|
+
tags.push({ tag: "meta", attrs: { name: "twitter:site", content: twitter } });
|
|
421
|
+
}
|
|
422
|
+
if (config.branding.favicon) {
|
|
423
|
+
tags.push({ tag: "link", attrs: { rel: "icon", href: config.branding.favicon } });
|
|
424
|
+
}
|
|
425
|
+
const lines = ["head:"];
|
|
426
|
+
for (const { tag, attrs } of tags) {
|
|
427
|
+
lines.push(` - tag: ${tag}`);
|
|
428
|
+
lines.push(` attrs:`);
|
|
429
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
430
|
+
lines.push(` ${k}: ${yamlString(v)}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return lines;
|
|
434
|
+
}
|
|
435
|
+
function renderStats(mode, stats) {
|
|
436
|
+
if (mode === "off") return [];
|
|
437
|
+
if (mode === "capability") {
|
|
438
|
+
const verified = stats.passed;
|
|
439
|
+
const inProgress = stats.failed + stats.skipped;
|
|
440
|
+
const items = [
|
|
441
|
+
` <ul class="demo-stats" aria-label="Coverage summary">`,
|
|
442
|
+
` <li class="demo-stat" data-tone="total"><span class="demo-stat__value">${stats.total}</span><span class="demo-stat__label">Scenarios</span></li>`,
|
|
443
|
+
` <li class="demo-stat" data-tone="pass"><span class="demo-stat__value">${verified}</span><span class="demo-stat__label">Verified</span></li>`
|
|
444
|
+
];
|
|
445
|
+
if (inProgress > 0) {
|
|
446
|
+
items.push(
|
|
447
|
+
` <li class="demo-stat" data-tone="pending"><span class="demo-stat__value">${inProgress}</span><span class="demo-stat__label">In progress</span></li>`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
items.push(` </ul>`);
|
|
451
|
+
return ["", ...items];
|
|
452
|
+
}
|
|
453
|
+
return [
|
|
454
|
+
"",
|
|
455
|
+
` <ul class="demo-stats" aria-label="Test results">`,
|
|
456
|
+
` <li class="demo-stat" data-tone="total"><span class="demo-stat__value">${stats.total}</span><span class="demo-stat__label">Scenarios</span></li>`,
|
|
457
|
+
` <li class="demo-stat" data-tone="pass"><span class="demo-stat__value">${stats.passed}</span><span class="demo-stat__label">Passed</span></li>`,
|
|
458
|
+
` <li class="demo-stat" data-tone="fail"><span class="demo-stat__value">${stats.failed}</span><span class="demo-stat__label">Failed</span></li>`,
|
|
459
|
+
` <li class="demo-stat" data-tone="skip"><span class="demo-stat__value">${stats.skipped}</span><span class="demo-stat__label">Skipped</span></li>`,
|
|
460
|
+
` </ul>`
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
function renderStoryList(pages, statusBySlug, heading) {
|
|
464
|
+
const lines = [
|
|
465
|
+
"",
|
|
466
|
+
` <section class="demo-section">`,
|
|
467
|
+
` <h2 class="demo-section-heading">${escapeHtml(heading)}</h2>`
|
|
468
|
+
];
|
|
469
|
+
if (pages.length === 0) {
|
|
470
|
+
lines.push(
|
|
471
|
+
` <div class="demo-empty">No stories generated yet. Run <code>executable-stories-demo build</code>.</div>`
|
|
472
|
+
);
|
|
473
|
+
} else {
|
|
474
|
+
lines.push(` <ol class="demo-stories">`);
|
|
475
|
+
pages.forEach((page, index) => {
|
|
476
|
+
const status = statusBySlug.get(page.slug) ?? "pending";
|
|
477
|
+
const indexLabel = String(index + 1).padStart(2, "0");
|
|
478
|
+
const title = escapeHtml(page.title);
|
|
479
|
+
const statusLabel = escapeHtml(status);
|
|
480
|
+
const href = toAstroUrl(page.slug);
|
|
481
|
+
lines.push(
|
|
482
|
+
` <li><a class="demo-story" data-status="${statusLabel}" href="${href}"><span class="demo-story__index">${indexLabel}</span><span class="demo-story__title">${title}</span><span class="demo-story__status">${statusLabel}</span></a></li>`
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
lines.push(` </ol>`);
|
|
486
|
+
}
|
|
487
|
+
lines.push(` </section>`);
|
|
488
|
+
return lines;
|
|
489
|
+
}
|
|
490
|
+
function findFeaturedMedia(featuredSlug, pages, run, docsDir, assetsBaseUrl) {
|
|
491
|
+
const target = pages.find((p) => p.slug === featuredSlug);
|
|
492
|
+
if (!target) return void 0;
|
|
493
|
+
const storiesDir = path.join(docsDir, "stories");
|
|
494
|
+
const baseDir = toPosix(path.resolve(storiesDir));
|
|
495
|
+
for (const tc of run.testCases) {
|
|
496
|
+
const out = computeStoryOutputPath(tc.sourceFile, baseDir, "index");
|
|
497
|
+
const rel = toPosix(path.posix.relative(toPosix(path.resolve(docsDir)), out));
|
|
498
|
+
const slug = rel.replace(/\.md$/, "");
|
|
499
|
+
if (slug !== featuredSlug) continue;
|
|
500
|
+
for (const att of tc.attachments) {
|
|
501
|
+
const isVideo = att.mediaType.startsWith("video/");
|
|
502
|
+
const isImage = att.mediaType.startsWith("image/");
|
|
503
|
+
if (!isVideo && !isImage) continue;
|
|
504
|
+
let src;
|
|
505
|
+
if (att.contentEncoding === "BASE64") {
|
|
506
|
+
src = `data:${att.mediaType};base64,${att.body}`;
|
|
507
|
+
} else if (att.body) {
|
|
508
|
+
const body = att.body.trim();
|
|
509
|
+
if (body.startsWith("http") || body.startsWith("data:") || body.startsWith("/")) {
|
|
510
|
+
src = body;
|
|
511
|
+
} else {
|
|
512
|
+
src = `${assetsBaseUrl}/${path.posix.basename(body)}`;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (!src) continue;
|
|
516
|
+
return {
|
|
517
|
+
kind: isVideo ? "video" : "image",
|
|
518
|
+
src,
|
|
519
|
+
alt: att.name || target.title,
|
|
520
|
+
storyHref: toAstroUrl(target.slug),
|
|
521
|
+
storyTitle: target.title
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
function renderFeatured(featured) {
|
|
528
|
+
const lines = [
|
|
529
|
+
"",
|
|
530
|
+
` <section class="demo-featured" aria-labelledby="demo-featured-title">`,
|
|
531
|
+
` <span class="demo-featured__eyebrow">Watch first</span>`,
|
|
532
|
+
` <h2 id="demo-featured-title" class="demo-featured__title">${escapeHtml(featured.storyTitle)}</h2>`,
|
|
533
|
+
` <div class="demo-featured__media">`
|
|
534
|
+
];
|
|
535
|
+
if (featured.kind === "video") {
|
|
536
|
+
lines.push(
|
|
537
|
+
` <video class="demo-featured__video" controls preload="metadata" src="${escapeHtml(featured.src)}" aria-label="${escapeHtml(featured.alt)}"></video>`
|
|
538
|
+
);
|
|
539
|
+
} else {
|
|
540
|
+
lines.push(
|
|
541
|
+
` <img class="demo-featured__image" src="${escapeHtml(featured.src)}" alt="${escapeHtml(featured.alt)}" />`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
lines.push(` </div>`);
|
|
545
|
+
lines.push(
|
|
546
|
+
` <a class="demo-featured__link" href="${escapeHtml(featured.storyHref)}">Read the full scenario \u2192</a>`
|
|
547
|
+
);
|
|
548
|
+
lines.push(` </section>`);
|
|
549
|
+
return lines;
|
|
550
|
+
}
|
|
551
|
+
function renderSections(sections) {
|
|
552
|
+
if (sections.length === 0) return [];
|
|
553
|
+
const lines = [];
|
|
554
|
+
for (const section of sections) {
|
|
555
|
+
lines.push("");
|
|
556
|
+
if (section.kind === "feature-grid") {
|
|
557
|
+
lines.push(` <section class="demo-section">`);
|
|
558
|
+
if (section.heading) {
|
|
559
|
+
lines.push(` <h2 class="demo-section-heading">${escapeHtml(section.heading)}</h2>`);
|
|
560
|
+
}
|
|
561
|
+
lines.push(` <ul class="demo-feature-grid">`);
|
|
562
|
+
for (const item of section.items) {
|
|
563
|
+
lines.push(` <li class="demo-feature">`);
|
|
564
|
+
lines.push(` <h3 class="demo-feature__title">${escapeHtml(item.title)}</h3>`);
|
|
565
|
+
lines.push(` <p class="demo-feature__body">${escapeHtml(item.body)}</p>`);
|
|
566
|
+
lines.push(` </li>`);
|
|
567
|
+
}
|
|
568
|
+
lines.push(` </ul>`);
|
|
569
|
+
lines.push(` </section>`);
|
|
570
|
+
} else if (section.kind === "narrative") {
|
|
571
|
+
lines.push(` <section class="demo-narrative">`);
|
|
572
|
+
lines.push(` <div class="demo-narrative__copy">`);
|
|
573
|
+
if (section.eyebrow) {
|
|
574
|
+
lines.push(` <span class="demo-narrative__eyebrow">${escapeHtml(section.eyebrow)}</span>`);
|
|
575
|
+
}
|
|
576
|
+
if (section.heading) {
|
|
577
|
+
lines.push(` <h2 class="demo-narrative__heading">${escapeHtml(section.heading)}</h2>`);
|
|
578
|
+
}
|
|
579
|
+
lines.push(` <p class="demo-narrative__body">${escapeHtml(section.body)}</p>`);
|
|
580
|
+
lines.push(` </div>`);
|
|
581
|
+
if (section.media) {
|
|
582
|
+
lines.push(` <div class="demo-narrative__media">`);
|
|
583
|
+
lines.push(` <img src="${escapeHtml(section.media)}" alt="" />`);
|
|
584
|
+
lines.push(` </div>`);
|
|
585
|
+
}
|
|
586
|
+
lines.push(` </section>`);
|
|
587
|
+
} else if (section.kind === "quote") {
|
|
588
|
+
lines.push(` <figure class="demo-quote">`);
|
|
589
|
+
lines.push(` <blockquote class="demo-quote__body">${escapeHtml(section.quote)}</blockquote>`);
|
|
590
|
+
if (section.attribution) {
|
|
591
|
+
lines.push(` <figcaption class="demo-quote__attribution">${escapeHtml(section.attribution)}</figcaption>`);
|
|
592
|
+
}
|
|
593
|
+
lines.push(` </figure>`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return lines;
|
|
597
|
+
}
|
|
598
|
+
function writeLandingPage(indexPath, config, pages, run, docsDir, assetsBaseUrl) {
|
|
599
|
+
const stats = computeStats(run);
|
|
600
|
+
const statusBySlug = buildStatusBySlug(run, docsDir);
|
|
601
|
+
const ctaHref = config.cta.url || "/";
|
|
602
|
+
const ctaLabel = escapeHtml(config.cta.primary);
|
|
603
|
+
const productName = escapeHtml(config.productName);
|
|
604
|
+
const tagline = escapeHtml(config.tagline);
|
|
605
|
+
const isSplash = config.template === "splash";
|
|
606
|
+
const frontmatter = [
|
|
607
|
+
"---",
|
|
608
|
+
`title: ${yamlString(config.productName)}`,
|
|
609
|
+
`description: ${yamlString(config.tagline)}`,
|
|
610
|
+
"template: splash"
|
|
611
|
+
];
|
|
612
|
+
for (const line of renderHead(config)) frontmatter.push(line);
|
|
613
|
+
frontmatter.push("---", "");
|
|
614
|
+
const accentVar = config.branding.accent ? ` style="--demo-accent-override: ${cssColorEscape(config.branding.accent)};"` : "";
|
|
615
|
+
const lines = [
|
|
616
|
+
...frontmatter,
|
|
617
|
+
`{/* Generated by executable-stories-demo. Edit demo.config.json, not this file. */}`,
|
|
618
|
+
`{/* template=${config.template} theme=${config.theme} stats=${config.stats.mode} */}`,
|
|
619
|
+
"",
|
|
620
|
+
`<div class="demo-landing not-content" data-template="${config.template}"${accentVar}>`,
|
|
621
|
+
"",
|
|
622
|
+
` <section class="demo-hero">`
|
|
623
|
+
];
|
|
624
|
+
if (config.branding.logo) {
|
|
625
|
+
lines.push(
|
|
626
|
+
` <img class="demo-hero__logo" src="${escapeHtml(config.branding.logo)}" alt="${escapeHtml(config.productName + " logo")}" />`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
lines.push(
|
|
630
|
+
` <span class="demo-hero__eyebrow">${escapeHtml(isSplash ? "Product demo" : "Executable stories")}</span>`,
|
|
631
|
+
` <h1 class="demo-hero__title">${productName}</h1>`,
|
|
632
|
+
` <p class="demo-hero__tagline">${tagline}</p>`,
|
|
633
|
+
` <a class="demo-hero__cta" href="${escapeHtml(ctaHref)}">${ctaLabel}</a>`,
|
|
634
|
+
` </section>`
|
|
635
|
+
);
|
|
636
|
+
if (isSplash && config.featured.scenario) {
|
|
637
|
+
const featured = findFeaturedMedia(
|
|
638
|
+
config.featured.scenario,
|
|
639
|
+
pages,
|
|
640
|
+
run,
|
|
641
|
+
docsDir,
|
|
642
|
+
assetsBaseUrl
|
|
643
|
+
);
|
|
644
|
+
if (featured) lines.push(...renderFeatured(featured));
|
|
645
|
+
}
|
|
646
|
+
for (const line of renderStats(config.stats.mode, stats)) lines.push(line);
|
|
647
|
+
if (isSplash) {
|
|
648
|
+
for (const line of renderSections(config.sections)) lines.push(line);
|
|
649
|
+
}
|
|
650
|
+
const storyHeading = isSplash ? "Scenarios" : "Stories";
|
|
651
|
+
for (const line of renderStoryList(pages, statusBySlug, storyHeading)) {
|
|
652
|
+
lines.push(line);
|
|
653
|
+
}
|
|
654
|
+
lines.push("");
|
|
655
|
+
lines.push("</div>");
|
|
656
|
+
fs.writeFileSync(indexPath, `${lines.join("\n")}
|
|
657
|
+
`, "utf8");
|
|
658
|
+
}
|
|
659
|
+
function yamlString(value) {
|
|
660
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
661
|
+
}
|
|
662
|
+
function cssColorEscape(value) {
|
|
663
|
+
return value.replace(/[^a-zA-Z0-9#%(),.\-\s/]/g, "");
|
|
664
|
+
}
|
|
665
|
+
function buildStatusBySlug(run, docsDir) {
|
|
666
|
+
const storiesDir = path.join(docsDir, "stories");
|
|
667
|
+
const baseDir = toPosix(path.resolve(storiesDir));
|
|
668
|
+
const absDocsDir = toPosix(path.resolve(docsDir));
|
|
669
|
+
const result = /* @__PURE__ */ new Map();
|
|
670
|
+
for (const tc of run.testCases) {
|
|
671
|
+
const outputPath = computeStoryOutputPath(tc.sourceFile, baseDir, "index");
|
|
672
|
+
const relative2 = toPosix(path.posix.relative(absDocsDir, outputPath));
|
|
673
|
+
const slug = relative2.replace(/\.md$/, "");
|
|
674
|
+
const current = result.get(slug);
|
|
675
|
+
result.set(slug, mergePageStatus(current, tc.status));
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
function mergePageStatus(current, incoming) {
|
|
680
|
+
if (!current) return incoming;
|
|
681
|
+
if (current === "failed" || incoming === "failed") return "failed";
|
|
682
|
+
if (current === "passed" || incoming === "passed") return "passed";
|
|
683
|
+
if (current === "skipped" && incoming === "skipped") return "skipped";
|
|
684
|
+
return current;
|
|
685
|
+
}
|
|
686
|
+
function escapeHtml(value) {
|
|
687
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
688
|
+
}
|
|
689
|
+
function toAstroUrl(pageSlug) {
|
|
690
|
+
const sanitized = pageSlug.split("/").map((segment) => segment.toLowerCase().replace(/[^a-z0-9_-]/g, "")).filter(Boolean).join("/");
|
|
691
|
+
return sanitized.length === 0 ? "/" : `/${sanitized}/`;
|
|
692
|
+
}
|
|
693
|
+
function copyDirRecursive(src, dest) {
|
|
694
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
695
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
696
|
+
for (const entry of entries) {
|
|
697
|
+
const srcPath = path.join(src, entry.name);
|
|
698
|
+
const destPath = path.join(dest, entry.name);
|
|
699
|
+
if (entry.isDirectory()) {
|
|
700
|
+
copyDirRecursive(srcPath, destPath);
|
|
701
|
+
} else {
|
|
702
|
+
fs.copyFileSync(srcPath, destPath);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function toTitleCase(value) {
|
|
707
|
+
return value.replace(/[-_]/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (match) => match.toUpperCase());
|
|
708
|
+
}
|
|
709
|
+
function toPosix(value) {
|
|
710
|
+
return value.split(path.sep).join("/");
|
|
711
|
+
}
|
|
712
|
+
function normalizeAssetsBaseUrl(value) {
|
|
713
|
+
const base = value ?? "/demo-assets";
|
|
714
|
+
if (!base.startsWith("/")) return `/${base.replace(/\/+$/, "")}`;
|
|
715
|
+
return base.replace(/\/+$/, "") || "/demo-assets";
|
|
716
|
+
}
|
|
717
|
+
function resolveAssetsDir(siteDir, value) {
|
|
718
|
+
if (!value) return path.join(siteDir, "public", "demo-assets");
|
|
719
|
+
if (path.isAbsolute(value)) return value;
|
|
720
|
+
return path.join(siteDir, value);
|
|
721
|
+
}
|
|
722
|
+
export {
|
|
723
|
+
buildDemo,
|
|
724
|
+
initDemo,
|
|
725
|
+
previewDemo
|
|
726
|
+
};
|
|
727
|
+
//# sourceMappingURL=index.js.map
|