@weasel-js/forge 1.9.3 → 1.9.4
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 +22 -0
- package/dist/{FrameController-CpcY0DNB.d.ts → FrameController-BQjKhdwk.d.ts} +2 -36
- package/dist/chunk-5DXUNLV2.js +6 -0
- package/dist/chunk-5DXUNLV2.js.map +1 -0
- package/dist/{chunk-MGQWCPYE.js → chunk-5KI5YSQ5.js} +4 -5
- package/dist/chunk-5KI5YSQ5.js.map +1 -0
- package/dist/{chunk-Q2EZXJFE.js → chunk-B3PPHJTS.js} +59 -45
- package/dist/chunk-B3PPHJTS.js.map +1 -0
- package/dist/{chunk-CJX3W5HY.js → chunk-FRUA5WJF.js} +4 -3
- package/dist/chunk-FRUA5WJF.js.map +1 -0
- package/dist/{chunk-GZPQMGXJ.js → chunk-JHL4ERDA.js} +4 -3
- package/dist/chunk-JHL4ERDA.js.map +1 -0
- package/dist/{chunk-JVRHBI3V.js → chunk-MQSFPRF6.js} +20 -44
- package/dist/chunk-MQSFPRF6.js.map +1 -0
- package/dist/chunk-TXLWQFF6.js +10 -0
- package/dist/chunk-TXLWQFF6.js.map +1 -0
- package/dist/{chunk-BGOHALHO.js → chunk-ZZCCIXEJ.js} +11 -90
- package/dist/chunk-ZZCCIXEJ.js.map +1 -0
- package/dist/{config-BfsYw5lg.d.ts → config-Dfu-NKxd.d.ts} +27 -4
- package/dist/csf/preview-api.js +2 -2
- package/dist/frame/index.d.ts +2 -2
- package/dist/frame/index.js +6 -40
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/shell/index.css +61 -61
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +5 -13
- package/dist/shell/index.js +345 -332
- package/dist/shell/index.js.map +1 -1
- package/dist/test/index.d.ts +4 -2
- package/dist/test/index.js +7 -7
- package/dist/test/index.js.map +1 -1
- package/dist/test/play.d.ts +3 -3
- package/dist/test/play.js.map +1 -1
- package/dist/vite/index.d.ts +3 -1
- package/dist/vite/index.js +365 -101
- package/dist/vite/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-BGOHALHO.js.map +0 -1
- package/dist/chunk-CJX3W5HY.js.map +0 -1
- package/dist/chunk-GZPQMGXJ.js.map +0 -1
- package/dist/chunk-JVRHBI3V.js.map +0 -1
- package/dist/chunk-KJGXJD3U.js +0 -15
- package/dist/chunk-KJGXJD3U.js.map +0 -1
- package/dist/chunk-MGQWCPYE.js.map +0 -1
- package/dist/chunk-Q2EZXJFE.js.map +0 -1
package/dist/vite/index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
|
|
1
2
|
import { isGallery } from '../chunk-WEQSLPKT.js';
|
|
2
3
|
import { storyNameFromExport, storyId } from '../chunk-HRGPFQBW.js';
|
|
3
4
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, globSync, statSync } from 'fs';
|
|
4
|
-
import { resolve, sep, relative,
|
|
5
|
+
import { resolve, sep, relative, dirname, join, basename, matchesGlob } from 'path';
|
|
5
6
|
import { parse } from '@babel/parser';
|
|
6
7
|
import { fileURLToPath } from 'url';
|
|
7
8
|
|
|
8
9
|
// src/vite/html.ts
|
|
9
|
-
|
|
10
|
+
var escaped = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
11
|
+
function html(entry, mode = "serve", title = DEFAULT_TITLE) {
|
|
10
12
|
const src = `${mode === "serve" ? "/@id/" : ""}virtual:forge/${entry}`;
|
|
11
13
|
return `<!doctype html>
|
|
12
14
|
<html lang="en">
|
|
13
15
|
<head>
|
|
14
16
|
<meta charset="utf-8" />
|
|
15
17
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
16
|
-
<title
|
|
18
|
+
<title>${escaped(title)}</title>
|
|
17
19
|
</head>
|
|
18
20
|
<body>
|
|
19
21
|
<div id="root"></div>
|
|
@@ -26,13 +28,13 @@ function html(entry, mode = "serve") {
|
|
|
26
28
|
// src/vite/build.ts
|
|
27
29
|
var DIR = "node_modules/.forge";
|
|
28
30
|
var PAGES = { index: "shell-entry.js", frame: "frame-entry.js" };
|
|
29
|
-
function writePages(root) {
|
|
31
|
+
function writePages(root, title) {
|
|
30
32
|
const dir = join(root, DIR);
|
|
31
33
|
mkdirSync(dir, { recursive: true });
|
|
32
34
|
return Object.fromEntries(
|
|
33
35
|
Object.entries(PAGES).map(([name, entry]) => {
|
|
34
36
|
const file = join(dir, `${name}.html`);
|
|
35
|
-
writeFileSync(file, html(entry, "build"));
|
|
37
|
+
writeFileSync(file, html(entry, "build", title));
|
|
36
38
|
return [name, file];
|
|
37
39
|
})
|
|
38
40
|
);
|
|
@@ -47,34 +49,6 @@ function hoistPages(bundle, emit) {
|
|
|
47
49
|
emit({ type: "asset", fileName: `${name}.html`, source: up(asset.source) });
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
|
-
var posix = (path) => path.split(sep).join("/");
|
|
51
|
-
var dotted = (path) => /^\.{1,2}([/\\]|$)/.test(path) ? path : `./${path}`;
|
|
52
|
-
function globDirectory(pattern) {
|
|
53
|
-
const segments = pattern.split("/");
|
|
54
|
-
const magic = segments.findIndex((segment) => /^!|(^|[^\\])[*?[{(]/.test(segment));
|
|
55
|
-
return magic === -1 ? dirname(pattern) : segments.slice(0, magic).join("/");
|
|
56
|
-
}
|
|
57
|
-
function titleParts(parts) {
|
|
58
|
-
const last = parts[parts.length - 1];
|
|
59
|
-
if (last === void 0) return parts;
|
|
60
|
-
const stripped = last.replace(/(?:[.](?:story|stories))?([.][^.]+)$/i, "");
|
|
61
|
-
if (parts.length === 1) return [stripped];
|
|
62
|
-
const nextToLast = parts[parts.length - 2];
|
|
63
|
-
if (stripped && nextToLast && stripped.toLowerCase() === nextToLast.toLowerCase()) return [...parts.slice(0, -2), stripped];
|
|
64
|
-
if (stripped && (/^(story|stories)([.][^.]+)$/i.test(last) || /^index$/i.test(stripped))) return parts.slice(0, -1);
|
|
65
|
-
return [...parts.slice(0, -1), stripped];
|
|
66
|
-
}
|
|
67
|
-
var titleFrom = (importPath, directory) => titleParts(importPath.replace(directory, "").split("/").filter(Boolean)).join("/");
|
|
68
|
-
function autoTitle(file, root, stories) {
|
|
69
|
-
const path = posix(relative(root, file));
|
|
70
|
-
const importPath = dotted(path);
|
|
71
|
-
for (const pattern of stories) {
|
|
72
|
-
if (!matchesGlob(path, pattern.replace(/^\.\//, ""))) continue;
|
|
73
|
-
const directory = dotted(posix(relative(root, resolve(root, globDirectory(pattern))))).replace(/\/$/, "");
|
|
74
|
-
return titleFrom(importPath, directory);
|
|
75
|
-
}
|
|
76
|
-
return titleFrom(importPath, ".");
|
|
77
|
-
}
|
|
78
52
|
var keyOf = (file, local) => `${file}#${local}`;
|
|
79
53
|
var fileOfKey = (key) => key.slice(0, key.lastIndexOf("#"));
|
|
80
54
|
var SCRIPT = /\.[cm]?[jt]sx?$/;
|
|
@@ -365,11 +339,59 @@ function calleeOf(node, bindings, depth = 0) {
|
|
|
365
339
|
return void 0;
|
|
366
340
|
}
|
|
367
341
|
}
|
|
342
|
+
var LITERALS = /* @__PURE__ */ new Set([
|
|
343
|
+
"StringLiteral",
|
|
344
|
+
"NumericLiteral",
|
|
345
|
+
"BooleanLiteral",
|
|
346
|
+
"NullLiteral",
|
|
347
|
+
"BigIntLiteral",
|
|
348
|
+
"RegExpLiteral",
|
|
349
|
+
"TemplateLiteral",
|
|
350
|
+
"ArrayExpression",
|
|
351
|
+
"ArrowFunctionExpression",
|
|
352
|
+
"FunctionExpression",
|
|
353
|
+
"FunctionDeclaration",
|
|
354
|
+
"ClassExpression",
|
|
355
|
+
"JSXElement",
|
|
356
|
+
"JSXFragment"
|
|
357
|
+
]);
|
|
358
|
+
function isLiteralValue(node, bindings, depth = 0) {
|
|
359
|
+
if (!node || depth > 16) return false;
|
|
360
|
+
switch (node.type) {
|
|
361
|
+
case "TSSatisfiesExpression":
|
|
362
|
+
case "TSAsExpression":
|
|
363
|
+
case "TSTypeAssertion":
|
|
364
|
+
case "TSNonNullExpression":
|
|
365
|
+
case "ParenthesizedExpression":
|
|
366
|
+
return isLiteralValue(node.expression, bindings, depth + 1);
|
|
367
|
+
case "Identifier":
|
|
368
|
+
return isLiteralValue(bindings.get(node.name), bindings, depth + 1);
|
|
369
|
+
case "ObjectExpression":
|
|
370
|
+
return node.properties.every((p) => p.type !== "SpreadElement");
|
|
371
|
+
default:
|
|
372
|
+
return LITERALS.has(node.type);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
368
375
|
function stringLiteral(node) {
|
|
369
376
|
if (node.type === "StringLiteral") return node.value;
|
|
370
377
|
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis.map((q) => q.value.cooked ?? q.value.raw).join("");
|
|
371
378
|
return void 0;
|
|
372
379
|
}
|
|
380
|
+
function stringOf(node, bindings, depth = 0) {
|
|
381
|
+
if (!node || depth > 16) return void 0;
|
|
382
|
+
switch (node.type) {
|
|
383
|
+
case "TSSatisfiesExpression":
|
|
384
|
+
case "TSAsExpression":
|
|
385
|
+
case "TSTypeAssertion":
|
|
386
|
+
case "TSNonNullExpression":
|
|
387
|
+
case "ParenthesizedExpression":
|
|
388
|
+
return stringOf(node.expression, bindings, depth + 1);
|
|
389
|
+
case "Identifier":
|
|
390
|
+
return stringOf(bindings.get(node.name), bindings, depth + 1);
|
|
391
|
+
default:
|
|
392
|
+
return stringLiteral(node);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
373
395
|
function isDefaultExport(stmt) {
|
|
374
396
|
if (stmt.type === "ExportDefaultDeclaration") return true;
|
|
375
397
|
return stmt.type === "ExportNamedDeclaration" && stmt.specifiers.some((s) => s.type === "ExportSpecifier" && exportedName(s.exported) === "default");
|
|
@@ -416,10 +438,58 @@ function stringProp(obj, key) {
|
|
|
416
438
|
return value?.type === "StringLiteral" ? value.value : void 0;
|
|
417
439
|
}
|
|
418
440
|
|
|
441
|
+
// src/vite/nativeExports.ts
|
|
442
|
+
var isFn = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression" || node?.type === "FunctionDeclaration";
|
|
443
|
+
var foreignFrom = (known) => ({
|
|
444
|
+
value: (name) => known.values[name],
|
|
445
|
+
call: (name) => known.calls[name],
|
|
446
|
+
reexport: (name) => known.reexports[name]
|
|
447
|
+
});
|
|
448
|
+
function returnsStory(statements, scope, depth) {
|
|
449
|
+
return statements.some((stmt) => {
|
|
450
|
+
if (stmt.type === "ReturnStatement") return mayBeStory(stmt.argument ?? void 0, scope, depth);
|
|
451
|
+
if (stmt.type === "BlockStatement") return returnsStory(stmt.body, scope, depth);
|
|
452
|
+
if (stmt.type !== "IfStatement") return false;
|
|
453
|
+
return returnsStory([stmt.consequent, ...stmt.alternate ? [stmt.alternate] : []], scope, depth);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function mayReturnStory(fn, scope, depth = 0) {
|
|
457
|
+
return fn.body.type === "BlockStatement" ? returnsStory(fn.body.body, scope, depth + 1) : mayBeStory(fn.body, scope, depth + 1);
|
|
458
|
+
}
|
|
459
|
+
function mayBeStory(node, scope, depth = 0) {
|
|
460
|
+
if (!node) return true;
|
|
461
|
+
if (depth > 16) return false;
|
|
462
|
+
switch (node.type) {
|
|
463
|
+
case "TSSatisfiesExpression":
|
|
464
|
+
case "TSAsExpression":
|
|
465
|
+
case "TSTypeAssertion":
|
|
466
|
+
case "TSNonNullExpression":
|
|
467
|
+
case "ParenthesizedExpression":
|
|
468
|
+
return mayBeStory(node.expression, scope, depth + 1);
|
|
469
|
+
case "Identifier":
|
|
470
|
+
return scope.bindings.has(node.name) ? mayBeStory(scope.bindings.get(node.name), scope, depth + 1) : scope.foreign?.value(node.name) ?? true;
|
|
471
|
+
case "CallExpression": {
|
|
472
|
+
if (node.callee.type !== "Identifier") return false;
|
|
473
|
+
if (scope.wrappers.has(node.callee.name)) return true;
|
|
474
|
+
const fn = scope.bindings.get(node.callee.name);
|
|
475
|
+
if (isFn(fn)) return mayReturnStory(fn, scope, depth);
|
|
476
|
+
return !scope.bindings.has(node.callee.name) && (scope.foreign?.call(node.callee.name) ?? false);
|
|
477
|
+
}
|
|
478
|
+
case "ConditionalExpression":
|
|
479
|
+
return mayBeStory(node.consequent, scope, depth + 1) || mayBeStory(node.alternate, scope, depth + 1);
|
|
480
|
+
case "LogicalExpression":
|
|
481
|
+
return mayBeStory(node.left, scope, depth + 1) || mayBeStory(node.right, scope, depth + 1);
|
|
482
|
+
case "ObjectExpression":
|
|
483
|
+
return node.properties.some((p) => p.type === "SpreadElement" && mayBeStory(p.argument, scope, depth + 1));
|
|
484
|
+
default:
|
|
485
|
+
return !isLiteralValue(node, scope.bindings);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
419
489
|
// src/vite/indexFile.ts
|
|
420
|
-
function indexFile(code, file, autoTitle2, wrappers = /* @__PURE__ */ new Set()) {
|
|
490
|
+
function indexFile(code, file, autoTitle2, wrappers = /* @__PURE__ */ new Set(), foreign) {
|
|
421
491
|
try {
|
|
422
|
-
return indexProgram(parseFile(code, file), file, autoTitle2, wrappers);
|
|
492
|
+
return indexProgram(parseFile(code, file), file, autoTitle2, wrappers, foreign);
|
|
423
493
|
} catch (err) {
|
|
424
494
|
throw new Error(`${file}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
425
495
|
}
|
|
@@ -464,11 +534,13 @@ function scan(program, extra) {
|
|
|
464
534
|
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
465
535
|
if (decl?.type === "VariableDeclaration") {
|
|
466
536
|
for (const d of decl.declarations) if (d.id.type === "Identifier" && d.init) bindings.set(d.id.name, d.init);
|
|
537
|
+
} else if (decl?.type === "FunctionDeclaration" && decl.id) {
|
|
538
|
+
bindings.set(decl.id.name, decl);
|
|
467
539
|
}
|
|
468
540
|
}
|
|
469
541
|
return { bindings, wrappers };
|
|
470
542
|
}
|
|
471
|
-
function indexProgram(program, file, autoTitle2, extra) {
|
|
543
|
+
function indexProgram(program, file, autoTitle2, extra, foreign) {
|
|
472
544
|
const { bindings, wrappers } = scan(program, extra);
|
|
473
545
|
let metaNode;
|
|
474
546
|
let metaDoc;
|
|
@@ -486,7 +558,7 @@ function indexProgram(program, file, autoTitle2, extra) {
|
|
|
486
558
|
if (d.id.type === "Identifier") exported.push({ name: d.id.name, node: d.init ?? void 0, doc: docOf(d) ?? doc });
|
|
487
559
|
}
|
|
488
560
|
} else if (decl?.type === "FunctionDeclaration" && decl.id) {
|
|
489
|
-
exported.push({ name: decl.id.name, node:
|
|
561
|
+
exported.push({ name: decl.id.name, node: decl, doc });
|
|
490
562
|
}
|
|
491
563
|
for (const spec of stmt.specifiers) {
|
|
492
564
|
if (spec.type !== "ExportSpecifier" || spec.exportKind === "type") continue;
|
|
@@ -495,12 +567,13 @@ function indexProgram(program, file, autoTitle2, extra) {
|
|
|
495
567
|
if (name === "default") {
|
|
496
568
|
metaNode = node;
|
|
497
569
|
metaDoc ??= doc;
|
|
498
|
-
} else exported.push({ name, node, doc });
|
|
570
|
+
} else if (stmt.source) exported.push({ name, node, doc, from: true });
|
|
571
|
+
else exported.push({ name, node: node ?? (spec.local.type === "Identifier" ? spec.local : void 0), doc });
|
|
499
572
|
}
|
|
500
573
|
}
|
|
501
574
|
if (!metaNode && !program.body.some(isDefaultExport)) return [];
|
|
502
575
|
const meta = objectOf(metaNode, bindings, wrappers);
|
|
503
|
-
const title =
|
|
576
|
+
const title = stringOf(prop(meta, "title"), bindings) ?? autoTitle2;
|
|
504
577
|
const include = storyFilter(meta, "includeStories");
|
|
505
578
|
const exclude = storyFilter(meta, "excludeStories");
|
|
506
579
|
const componentNode = prop(meta, "component");
|
|
@@ -508,7 +581,7 @@ function indexProgram(program, file, autoTitle2, extra) {
|
|
|
508
581
|
const metaIsolate = isolateOf(meta, bindings, wrappers, "default");
|
|
509
582
|
const metaTags = tagsOf(meta);
|
|
510
583
|
const native = wrappers.has(calleeOf(metaNode, bindings) ?? "");
|
|
511
|
-
return exported.filter(({ name }) => name !== "__namedExportsOrder").filter(({ name }) => (!include || include(name)) && (!exclude || !exclude(name))).map(({ name, node, doc }) => {
|
|
584
|
+
return exported.filter(({ name }) => name !== "__namedExportsOrder").filter(({ name }) => (!include || include(name)) && (!exclude || !exclude(name))).filter((e) => !native || (e.from ? foreign?.reexport(e.name) ?? true : mayBeStory(e.node, { bindings, wrappers, foreign }))).map(({ name, node, doc }) => {
|
|
512
585
|
const spec = objectOf(node, bindings, wrappers);
|
|
513
586
|
const isolate = isolateOf(spec, bindings, wrappers, name) ?? metaIsolate;
|
|
514
587
|
const tags = combineTags(metaTags, tagsOf(spec));
|
|
@@ -569,36 +642,105 @@ function storyFilter(obj, key) {
|
|
|
569
642
|
}
|
|
570
643
|
return null;
|
|
571
644
|
}
|
|
572
|
-
function
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
645
|
+
function storyGlobOf(path, stories) {
|
|
646
|
+
return stories.find((pattern) => matchesGlob(path, pattern.replace(/^\.\//, "")));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/vite/autoTitle.ts
|
|
650
|
+
var posix = (path) => path.split(sep).join("/");
|
|
651
|
+
var dotted = (path) => /^\.{1,2}([/\\]|$)/.test(path) ? path : `./${path}`;
|
|
652
|
+
function globDirectory(pattern) {
|
|
653
|
+
const segments = pattern.split("/");
|
|
654
|
+
const magic = segments.findIndex((segment) => /^!|(^|[^\\])[*?[{(]/.test(segment));
|
|
655
|
+
return magic === -1 ? dirname(pattern) : segments.slice(0, magic).join("/");
|
|
656
|
+
}
|
|
657
|
+
function titleParts(parts) {
|
|
658
|
+
const last = parts[parts.length - 1];
|
|
659
|
+
if (last === void 0) return parts;
|
|
660
|
+
const stripped = last.replace(/(?:[.](?:story|stories))?([.][^.]+)$/i, "");
|
|
661
|
+
if (parts.length === 1) return [stripped];
|
|
662
|
+
const nextToLast = parts[parts.length - 2];
|
|
663
|
+
if (stripped && nextToLast && stripped.toLowerCase() === nextToLast.toLowerCase()) return [...parts.slice(0, -2), stripped];
|
|
664
|
+
if (stripped && (/^(story|stories)([.][^.]+)$/i.test(last) || /^index$/i.test(stripped))) return parts.slice(0, -1);
|
|
665
|
+
return [...parts.slice(0, -1), stripped];
|
|
666
|
+
}
|
|
667
|
+
var titleFrom = (importPath, directory) => titleParts(importPath.replace(directory, "").split("/").filter(Boolean)).join("/");
|
|
668
|
+
function autoTitle(file, root, stories) {
|
|
669
|
+
const path = posix(relative(root, file));
|
|
670
|
+
const importPath = dotted(path);
|
|
671
|
+
const pattern = storyGlobOf(path, stories);
|
|
672
|
+
if (pattern === void 0) return titleFrom(importPath, ".");
|
|
673
|
+
const directory = dotted(posix(relative(root, resolve(root, globDirectory(pattern))))).replace(/\/$/, "");
|
|
674
|
+
return titleFrom(importPath, directory);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/vite/foreignStories.ts
|
|
678
|
+
var MAX_DEPTH = 8;
|
|
679
|
+
async function settled(read, ask) {
|
|
680
|
+
const known = { values: {}, calls: {}, reexports: {} };
|
|
681
|
+
const asked = /* @__PURE__ */ new Set();
|
|
682
|
+
for (; ; ) {
|
|
683
|
+
const fresh = [];
|
|
684
|
+
const answer = (kind) => (name) => {
|
|
685
|
+
if (!asked.has(`${kind}:${name}`)) fresh.push([kind, name]);
|
|
686
|
+
return known[kind][name];
|
|
687
|
+
};
|
|
688
|
+
const result = read({ value: answer("values"), call: answer("calls"), reexport: answer("reexports") });
|
|
689
|
+
if (fresh.length === 0) return { result, known };
|
|
690
|
+
for (const [kind, name] of fresh) {
|
|
691
|
+
asked.add(`${kind}:${name}`);
|
|
692
|
+
const found = await ask(kind, name);
|
|
693
|
+
if (found !== void 0) known[kind][name] = found;
|
|
578
694
|
}
|
|
579
|
-
const source = !relative(join(dir, "src"), here).startsWith("..");
|
|
580
|
-
return realms.map((realm) => join(dir, source ? `src/${realm}/index.ts` : `dist/${realm}/index.js`));
|
|
581
695
|
}
|
|
582
|
-
return [];
|
|
583
696
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const hit = SHIMS.find(([pattern]) => pattern.test(id));
|
|
594
|
-
if (!hit) return void 0;
|
|
595
|
-
return this.resolve(hit[1], fileURLToPath(import.meta.url), { skipSelf: true });
|
|
697
|
+
async function foreignStories(file, wrappers, own, trace, read) {
|
|
698
|
+
const answers = /* @__PURE__ */ new Map();
|
|
699
|
+
const exported = async (at, name, called, depth) => {
|
|
700
|
+
const key = await trace.declaring(at, name);
|
|
701
|
+
if (!key) return void 0;
|
|
702
|
+
if (own.has(key)) return called && own.get(key) === "story";
|
|
703
|
+
const memo = `${key}:${called}`;
|
|
704
|
+
if (!answers.has(memo)) {
|
|
705
|
+
answers.set(memo, depth > MAX_DEPTH ? Promise.resolve(void 0) : declared(key, called, depth));
|
|
596
706
|
}
|
|
707
|
+
return answers.get(memo);
|
|
708
|
+
};
|
|
709
|
+
const declared = async (key, called, depth) => {
|
|
710
|
+
const at = key.slice(0, key.lastIndexOf("#"));
|
|
711
|
+
const local = key.slice(at.length + 1);
|
|
712
|
+
let scope;
|
|
713
|
+
try {
|
|
714
|
+
scope = scan(parseFile(read(at), at), /* @__PURE__ */ new Set());
|
|
715
|
+
} catch {
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
const node = scope.bindings.get(local);
|
|
719
|
+
if (!node) return void 0;
|
|
720
|
+
const { result } = await settled(
|
|
721
|
+
(foreign) => called ? isFn(node) && mayReturnStory(node, { ...scope, foreign }) : mayBeStory(node, { ...scope, foreign }),
|
|
722
|
+
asking(at, depth + 1)
|
|
723
|
+
);
|
|
724
|
+
return result;
|
|
597
725
|
};
|
|
726
|
+
const asking = (at, depth) => async (kind, name) => {
|
|
727
|
+
const mod = trace.info(at);
|
|
728
|
+
const exp = mod?.exports.get(name);
|
|
729
|
+
const from = kind === "reexports" ? exp?.kind === "from" ? exp : void 0 : mod?.imports.get(name);
|
|
730
|
+
if (!from || from.imported === "*") return void 0;
|
|
731
|
+
const target = await trace.target(from.spec, at);
|
|
732
|
+
return target ? exported(target, from.imported, kind === "calls", depth) : void 0;
|
|
733
|
+
};
|
|
734
|
+
try {
|
|
735
|
+
const program = parseFile(read(file), file);
|
|
736
|
+
return (await settled((foreign) => void indexProgram(program, file, "", wrappers, foreign), asking(file, 0))).known;
|
|
737
|
+
} catch {
|
|
738
|
+
return { values: {}, calls: {}, reexports: {} };
|
|
739
|
+
}
|
|
598
740
|
}
|
|
599
741
|
|
|
600
|
-
// src/vite/
|
|
601
|
-
function
|
|
742
|
+
// src/vite/trace.ts
|
|
743
|
+
function createTracer(read) {
|
|
602
744
|
const modules = /* @__PURE__ */ new Map();
|
|
603
745
|
const info = (file) => {
|
|
604
746
|
if (!modules.has(file)) {
|
|
@@ -612,18 +754,20 @@ function createWrapperResolver(read) {
|
|
|
612
754
|
}
|
|
613
755
|
return modules.get(file) ?? null;
|
|
614
756
|
};
|
|
615
|
-
return
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
757
|
+
return (resolve4) => {
|
|
758
|
+
const trace = {
|
|
759
|
+
reads: /* @__PURE__ */ new Set(),
|
|
760
|
+
unresolved: false,
|
|
761
|
+
info,
|
|
762
|
+
async target(spec, importer) {
|
|
763
|
+
const id = (await resolve4(spec, importer).catch(() => null))?.split("?")[0];
|
|
764
|
+
if (!id) trace.unresolved = true;
|
|
765
|
+
return id && !id.startsWith("\0") ? id : null;
|
|
766
|
+
},
|
|
767
|
+
declaring: (at, name) => declaring(at, name, 0)
|
|
624
768
|
};
|
|
625
|
-
const declaring = async (at, name, depth
|
|
626
|
-
reads.add(at);
|
|
769
|
+
const declaring = async (at, name, depth) => {
|
|
770
|
+
trace.reads.add(at);
|
|
627
771
|
const mod = info(at);
|
|
628
772
|
if (!mod || depth > 32) return null;
|
|
629
773
|
const exp = mod.exports.get(name);
|
|
@@ -631,38 +775,121 @@ function createWrapperResolver(read) {
|
|
|
631
775
|
const imp = mod.imports.get(exp.local);
|
|
632
776
|
if (!imp) return `${at}#${exp.local}`;
|
|
633
777
|
if (imp.imported === "*") return null;
|
|
634
|
-
const next = await target(imp.spec, at);
|
|
778
|
+
const next = await trace.target(imp.spec, at);
|
|
635
779
|
return next ? declaring(next, imp.imported, depth + 1) : null;
|
|
636
780
|
}
|
|
637
781
|
if (exp?.kind === "from") {
|
|
638
|
-
const next = await target(exp.spec, at);
|
|
782
|
+
const next = await trace.target(exp.spec, at);
|
|
639
783
|
return next ? declaring(next, exp.imported, depth + 1) : null;
|
|
640
784
|
}
|
|
641
785
|
if (name === "default") return null;
|
|
642
786
|
for (const spec of mod.stars) {
|
|
643
|
-
const next = await target(spec, at);
|
|
787
|
+
const next = await trace.target(spec, at);
|
|
644
788
|
const found = next ? await declaring(next, name, depth + 1) : null;
|
|
645
789
|
if (found) return found;
|
|
646
790
|
}
|
|
647
791
|
return null;
|
|
648
792
|
};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
793
|
+
return trace;
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/vite/wrappers.ts
|
|
798
|
+
var NO_FOREIGN = { values: {}, calls: {}, reexports: {} };
|
|
799
|
+
function sameResolution(a, b) {
|
|
800
|
+
const key = (r) => JSON.stringify([[...r?.wrappers ?? []].sort(), r?.foreign ?? NO_FOREIGN]);
|
|
801
|
+
return key(a) === key(b);
|
|
802
|
+
}
|
|
803
|
+
function createWrapperResolver(read) {
|
|
804
|
+
const tracer = createTracer(read);
|
|
805
|
+
return async (file, callees, resolve4) => {
|
|
806
|
+
const trace = tracer(resolve4);
|
|
807
|
+
const wrappers = /* @__PURE__ */ new Set();
|
|
808
|
+
const direct = [...trace.info(file)?.imports.values() ?? []].some((imp) => imp.spec === "@weasel-js/forge");
|
|
809
|
+
if (callees.length === 0 && !direct) return { wrappers, foreign: NO_FOREIGN, reads: trace.reads, unresolved: false };
|
|
810
|
+
const forge2 = await trace.target("@weasel-js/forge", file);
|
|
811
|
+
const own = /* @__PURE__ */ new Map();
|
|
812
|
+
if (forge2) {
|
|
813
|
+
for (const name of ["meta", "story"]) {
|
|
814
|
+
const key = await trace.declaring(forge2, name);
|
|
815
|
+
if (key) own.set(key, name);
|
|
816
|
+
}
|
|
817
|
+
for (const callee of callees) {
|
|
818
|
+
const from = await trace.target(callee.spec, file);
|
|
819
|
+
const key = from ? await trace.declaring(from, callee.imported) : null;
|
|
820
|
+
if (key && own.has(key)) wrappers.add(callee.local);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const foreign = forge2 ? await foreignStories(file, wrappers, own, trace, read) : NO_FOREIGN;
|
|
824
|
+
return { wrappers, foreign, reads: trace.reads, unresolved: trace.unresolved };
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/vite/indexStory.ts
|
|
829
|
+
function indexStory(code, file, source, resolved) {
|
|
830
|
+
const foreign = resolved ? foreignFrom(resolved.foreign) : void 0;
|
|
831
|
+
return indexFile(code, file, autoTitle(file, source.root, source.stories), resolved?.wrappers, foreign);
|
|
832
|
+
}
|
|
833
|
+
async function indexStoryResolved(code, file, source, resolve4, decide = createWrapperResolver((path) => readFileSync(path, "utf8"))) {
|
|
834
|
+
const resolution = await decide(file, foreignCallees(code, file), resolve4);
|
|
835
|
+
return { entries: indexStory(code, file, source, resolution), resolution };
|
|
836
|
+
}
|
|
837
|
+
function ownEntries(realms) {
|
|
838
|
+
const here = fileURLToPath(import.meta.url);
|
|
839
|
+
for (let dir = dirname(here); dir !== dirname(dir); dir = dirname(dir)) {
|
|
840
|
+
const manifest = join(dir, "package.json");
|
|
841
|
+
if (!existsSync(manifest) || JSON.parse(readFileSync(manifest, "utf8")).name !== "@weasel-js/forge") {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const source = !relative(join(dir, "src"), here).startsWith("..");
|
|
845
|
+
return realms.map((realm) => join(dir, source ? `src/${realm}/index.ts` : `dist/${realm}/index.js`));
|
|
846
|
+
}
|
|
847
|
+
return [];
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/vite/shellTitle.ts
|
|
851
|
+
function shellTitle(code, file) {
|
|
852
|
+
let program;
|
|
853
|
+
try {
|
|
854
|
+
program = parseFile(code, file);
|
|
855
|
+
} catch {
|
|
856
|
+
return void 0;
|
|
857
|
+
}
|
|
858
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
859
|
+
let config;
|
|
860
|
+
for (const stmt of program.body) {
|
|
861
|
+
if (stmt.type === "VariableDeclaration") {
|
|
862
|
+
for (const d of stmt.declarations) if (d.id.type === "Identifier" && d.init) bindings.set(d.id.name, d.init);
|
|
863
|
+
}
|
|
864
|
+
if (stmt.type === "ExportDefaultDeclaration") config = stmt.declaration;
|
|
865
|
+
if (stmt.type !== "ExportNamedDeclaration" || stmt.source) continue;
|
|
866
|
+
for (const spec of stmt.specifiers) {
|
|
867
|
+
if (spec.type === "ExportSpecifier" && exportedName(spec.exported) === "default") config = spec.local;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const wrapper = calleeOf(config, bindings);
|
|
871
|
+
return stringOf(prop(objectOf(config, bindings, new Set(wrapper ? [wrapper] : [])), "title"), bindings);
|
|
872
|
+
}
|
|
873
|
+
var SHIMS = [
|
|
874
|
+
[/^@?storybook\/preview-api$/, "@weasel-js/forge/preview-api"],
|
|
875
|
+
[/^@?storybook\/test$/, "@weasel-js/forge/play"]
|
|
876
|
+
];
|
|
877
|
+
function storybookShims() {
|
|
878
|
+
return {
|
|
879
|
+
name: "weaselforge:storybook-shims",
|
|
880
|
+
enforce: "pre",
|
|
881
|
+
resolveId(id) {
|
|
882
|
+
const hit = SHIMS.find(([pattern]) => pattern.test(id));
|
|
883
|
+
if (!hit) return void 0;
|
|
884
|
+
return this.resolve(hit[1], fileURLToPath(import.meta.url), { skipSelf: true });
|
|
658
885
|
}
|
|
659
|
-
return { wrappers, reads, unresolved };
|
|
660
886
|
};
|
|
661
887
|
}
|
|
662
888
|
|
|
663
889
|
// src/vite/plugin.ts
|
|
664
890
|
var PREFIX = "virtual:forge/";
|
|
665
891
|
var MODULES = /* @__PURE__ */ new Set(["index.js", "importers.js", "deps.js", "frame-config.js", "shell-config.js", "shell-entry.js", "frame-entry.js"]);
|
|
892
|
+
var SCRIPT2 = /\.[cm]?[jt]sx?$/;
|
|
666
893
|
var PAGES2 = { "/": "shell-entry.js", "/index.html": "shell-entry.js", "/frame.html": "frame-entry.js" };
|
|
667
894
|
function forge(options) {
|
|
668
895
|
let root = process.cwd();
|
|
@@ -682,9 +909,9 @@ function forge(options) {
|
|
|
682
909
|
};
|
|
683
910
|
const matches = (file) => {
|
|
684
911
|
const path = relative(root, file).split(sep).join("/");
|
|
685
|
-
return !path.split("/").includes("node_modules") && options.stories
|
|
912
|
+
return !path.split("/").includes("node_modules") && storyGlobOf(path, options.stories) !== void 0;
|
|
686
913
|
};
|
|
687
|
-
const read = (file) =>
|
|
914
|
+
const read = (file) => indexStory(readFileSync(file, "utf8"), file, { root, stories: options.stories }, wrappersOf.get(file));
|
|
688
915
|
const logError = (err) => logger?.error(`[forge] ${err instanceof Error ? err.message : String(err)}`);
|
|
689
916
|
const files = () => {
|
|
690
917
|
byFile ??= new Map(
|
|
@@ -711,9 +938,9 @@ function forge(options) {
|
|
|
711
938
|
} catch {
|
|
712
939
|
continue;
|
|
713
940
|
}
|
|
714
|
-
const before = wrappersOf.get(file)
|
|
941
|
+
const before = wrappersOf.get(file);
|
|
715
942
|
wrappersOf.set(file, next);
|
|
716
|
-
if (before
|
|
943
|
+
if (sameResolution(before, next)) continue;
|
|
717
944
|
try {
|
|
718
945
|
current.set(file, read(file));
|
|
719
946
|
} catch (err) {
|
|
@@ -722,6 +949,15 @@ function forge(options) {
|
|
|
722
949
|
}
|
|
723
950
|
};
|
|
724
951
|
const depGraph = () => deps ??= createDepGraph({ root, read: (file) => readFileSync(file, "utf8") });
|
|
952
|
+
const titleUnder = (at) => {
|
|
953
|
+
if (!options.shellConfig) return void 0;
|
|
954
|
+
const file = resolve(at, options.shellConfig);
|
|
955
|
+
try {
|
|
956
|
+
return shellTitle(readFileSync(file, "utf8"), file);
|
|
957
|
+
} catch {
|
|
958
|
+
return void 0;
|
|
959
|
+
}
|
|
960
|
+
};
|
|
725
961
|
const configModule = (path) => path ? `export { default } from ${JSON.stringify(resolve(root, path))};
|
|
726
962
|
` : "export default {};\n";
|
|
727
963
|
const modules = {
|
|
@@ -756,8 +992,9 @@ import.meta.hot?.on('forge:index', (next) => workshop.setIndex(next));
|
|
|
756
992
|
// Read from source on first request, so the graph costs nothing until the page is up.
|
|
757
993
|
import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
|
|
758
994
|
import.meta.hot?.on('forge:deps', (next) => workshop.setDependencies(next));
|
|
759
|
-
//
|
|
760
|
-
// timestamps; the
|
|
995
|
+
// An edit to a story file, or to a module one imports, reaches here as an update of the importers module, whose
|
|
996
|
+
// fresh import() URLs carry the new timestamps; the files it reaches arrive just before, so the reload waits for
|
|
997
|
+
// the importers that can fetch them.
|
|
761
998
|
const stale = new Set();
|
|
762
999
|
import.meta.hot?.on('forge:story', ({ file }) => stale.add(file));
|
|
763
1000
|
import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
|
|
@@ -769,7 +1006,8 @@ import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
|
|
|
769
1006
|
`,
|
|
770
1007
|
"frame-entry.js": () => `import { mountFrame } from '@weasel-js/forge/frame';
|
|
771
1008
|
import '@weasel-js/forge/frame.css';
|
|
772
|
-
|
|
1009
|
+
${(options.frameStyles ?? []).map((path) => `import ${JSON.stringify(resolve(root, path))};
|
|
1010
|
+
`).join("")}import index from 'virtual:forge/index.js';
|
|
773
1011
|
import importers from 'virtual:forge/importers.js';
|
|
774
1012
|
import setup from 'virtual:forge/frame-config.js';
|
|
775
1013
|
|
|
@@ -801,13 +1039,28 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
|
|
|
801
1039
|
depsPending = null;
|
|
802
1040
|
});
|
|
803
1041
|
}
|
|
1042
|
+
const storiesReaching = (changed) => {
|
|
1043
|
+
const current = files();
|
|
1044
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1045
|
+
const reached = /* @__PURE__ */ new Set();
|
|
1046
|
+
const climb = (mod) => {
|
|
1047
|
+
if (seen.has(mod)) return;
|
|
1048
|
+
seen.add(mod);
|
|
1049
|
+
if (mod.file && current.has(mod.file)) reached.add(mod.file);
|
|
1050
|
+
if (mod.isSelfAccepting && !(mod.file && SCRIPT2.test(mod.file))) return;
|
|
1051
|
+
for (const importer of mod.importers) climb(importer);
|
|
1052
|
+
};
|
|
1053
|
+
for (const mod of changed) climb(mod);
|
|
1054
|
+
return [...reached];
|
|
1055
|
+
};
|
|
804
1056
|
async function reindex(server, file, event) {
|
|
805
1057
|
const current = files();
|
|
806
|
-
if (event === "change" && current.has(file)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
|
|
807
1058
|
const own = event === "add" ? matches(file) : current.has(file);
|
|
808
1059
|
const through = [...wrappersOf].filter(([story, { reads, unresolved }]) => story !== file && (reads.has(file) || event === "add" && unresolved)).map(([story]) => story);
|
|
809
1060
|
if (!own && through.length === 0) return false;
|
|
810
1061
|
const before = JSON.stringify(index());
|
|
1062
|
+
const importable = () => JSON.stringify([...current].filter(([, entries]) => entries.length > 0).map(([path]) => path));
|
|
1063
|
+
const importableBefore = importable();
|
|
811
1064
|
if (own && event === "unlink") {
|
|
812
1065
|
current.delete(file);
|
|
813
1066
|
wrappersOf.delete(file);
|
|
@@ -825,6 +1078,8 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
|
|
|
825
1078
|
if (JSON.stringify(next) === before) return false;
|
|
826
1079
|
invalidate(server, ["index.js", "importers.js"]);
|
|
827
1080
|
server.ws.send({ type: "custom", event: "forge:index", data: next });
|
|
1081
|
+
const importers = server.environments.client.moduleGraph.getModuleById(`\0${PREFIX}importers.js`);
|
|
1082
|
+
if (importers && importable() !== importableBefore) await server.environments.client.reloadModule(importers);
|
|
828
1083
|
return true;
|
|
829
1084
|
}
|
|
830
1085
|
return [
|
|
@@ -840,7 +1095,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
|
|
|
840
1095
|
];
|
|
841
1096
|
const optimizeDeps = { entries };
|
|
842
1097
|
if (command !== "build") return { optimizeDeps };
|
|
843
|
-
return { optimizeDeps, build: { rolldownOptions: { input: writePages(at) } } };
|
|
1098
|
+
return { optimizeDeps, build: { rolldownOptions: { input: writePages(at, titleUnder(at)) } } };
|
|
844
1099
|
},
|
|
845
1100
|
generateBundle: {
|
|
846
1101
|
order: "post",
|
|
@@ -874,12 +1129,16 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
|
|
|
874
1129
|
return `export default ${depsSent};
|
|
875
1130
|
`;
|
|
876
1131
|
},
|
|
1132
|
+
hotUpdate({ modules: modules2, server }) {
|
|
1133
|
+
if (this.environment.name !== "client") return;
|
|
1134
|
+
for (const file of storiesReaching(modules2)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
|
|
1135
|
+
},
|
|
877
1136
|
configureServer(server) {
|
|
878
1137
|
server.watcher.add([...files().keys()]);
|
|
879
1138
|
for (const event of ["add", "change", "unlink"]) {
|
|
880
1139
|
server.watcher.on(event, (path) => {
|
|
881
1140
|
const file = resolve(path);
|
|
882
|
-
const added = event === "add" &&
|
|
1141
|
+
const added = event === "add" && SCRIPT2.test(file) && !file.split(sep).includes("node_modules");
|
|
883
1142
|
if (added) depGraph().reset();
|
|
884
1143
|
const read2 = added || event !== "add" && depGraph().invalidate(file);
|
|
885
1144
|
reindex(server, file, event).then((indexed) => {
|
|
@@ -894,7 +1153,7 @@ import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
|
|
|
894
1153
|
const page = path === prefix ? "/" : path.startsWith(`${prefix}/`) ? path.slice(prefix.length) : null;
|
|
895
1154
|
const entry = req.method === "GET" && page !== null ? PAGES2[page] : void 0;
|
|
896
1155
|
if (!entry) return next();
|
|
897
|
-
server.transformIndexHtml(url, html(entry)).then(
|
|
1156
|
+
server.transformIndexHtml(url, html(entry, "serve", titleUnder(root))).then(
|
|
898
1157
|
(doc) => {
|
|
899
1158
|
res.statusCode = 200;
|
|
900
1159
|
res.setHeader("Content-Type", "text/html");
|
|
@@ -914,7 +1173,7 @@ function forgeTest(options) {
|
|
|
914
1173
|
if (id.startsWith("\0") || id.includes("?")) return false;
|
|
915
1174
|
const path = relative(root, id).split(sep).join("/");
|
|
916
1175
|
if (path.startsWith("../") || path.split("/").includes("node_modules")) return false;
|
|
917
|
-
return options.stories
|
|
1176
|
+
return storyGlobOf(path, options.stories) !== void 0;
|
|
918
1177
|
};
|
|
919
1178
|
return [
|
|
920
1179
|
...options.storybookShims === false ? [] : [storybookShims()],
|
|
@@ -933,9 +1192,14 @@ function forgeTest(options) {
|
|
|
933
1192
|
configResolved(config) {
|
|
934
1193
|
root = config.root;
|
|
935
1194
|
},
|
|
936
|
-
transform(code, id) {
|
|
1195
|
+
async transform(code, id) {
|
|
937
1196
|
if (!matches(id)) return void 0;
|
|
938
|
-
const entries =
|
|
1197
|
+
const { entries } = await indexStoryResolved(
|
|
1198
|
+
code,
|
|
1199
|
+
id,
|
|
1200
|
+
{ root, stories: options.stories },
|
|
1201
|
+
async (spec, importer) => (await this.resolve(spec, importer))?.id ?? null
|
|
1202
|
+
);
|
|
939
1203
|
const title = entries[0]?.title;
|
|
940
1204
|
if (title === void 0) return void 0;
|
|
941
1205
|
const q = (value) => JSON.stringify(value);
|