@orbytes/astrolab 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +410 -0
  3. package/bin/lab-cull.mjs +401 -0
  4. package/bin/pin-gallery.mjs +121 -0
  5. package/defaults.mjs +120 -0
  6. package/dist/core/astro-integration.js +130 -0
  7. package/dist/core/index.js +6 -0
  8. package/dist/core/lib-paths.js +29 -0
  9. package/dist/core/options.js +173 -0
  10. package/dist/core/utils/get-exports.js +52 -0
  11. package/dist/core/utils/invariant.js +12 -0
  12. package/dist/core/utils/kebab-case.js +15 -0
  13. package/dist/core/utils/path-builder.js +22 -0
  14. package/dist/core/utils/path.js +53 -0
  15. package/dist/core/virtual-module/get-story-modules.js +60 -0
  16. package/dist/core/virtual-module/story-modules.js +8 -0
  17. package/dist/core/virtual-module/virtual-module-ids.js +22 -0
  18. package/dist/core/virtual-module/virtual-routes.js +83 -0
  19. package/dist/core/virtual-module/vite-plugin.js +98 -0
  20. package/docs/PIN-CONTRACT.md +263 -0
  21. package/docs/PIN.md +429 -0
  22. package/index.d.ts +279 -0
  23. package/index.mjs +347 -0
  24. package/package.json +93 -0
  25. package/src/Empty.astro +4 -0
  26. package/src/Home.astro +298 -0
  27. package/src/LabHead.astro +1102 -0
  28. package/src/core/LICENSE-astrobook +166 -0
  29. package/src/core/astro-integration.ts +166 -0
  30. package/src/core/client.ts +89 -0
  31. package/src/core/index.ts +7 -0
  32. package/src/core/lib/components/empty.astro +1 -0
  33. package/src/core/lib/components/head.astro +1 -0
  34. package/src/core/lib/components/home.astro +8 -0
  35. package/src/core/lib/components/with-decorators.astro +22 -0
  36. package/src/core/lib/pages/app.astro +19 -0
  37. package/src/core/lib/pages/preview.astro +17 -0
  38. package/src/core/lib/pages/story.astro +16 -0
  39. package/src/core/lib-paths.ts +72 -0
  40. package/src/core/options.ts +262 -0
  41. package/src/core/utils/get-exports.ts +59 -0
  42. package/src/core/utils/invariant.ts +13 -0
  43. package/src/core/utils/kebab-case.ts +30 -0
  44. package/src/core/utils/path-builder.ts +45 -0
  45. package/src/core/utils/path.ts +80 -0
  46. package/src/core/virtual-module/get-story-modules.ts +110 -0
  47. package/src/core/virtual-module/story-modules.ts +9 -0
  48. package/src/core/virtual-module/virtual-module-ids.ts +17 -0
  49. package/src/core/virtual-module/virtual-routes.ts +130 -0
  50. package/src/core/virtual-module/vite-plugin.ts +125 -0
  51. package/src/pin/board.mjs +1521 -0
  52. package/src/pin/index.mjs +666 -0
  53. package/src/pin/shot.mjs +427 -0
  54. package/src/pin/source-stamp.mjs +159 -0
  55. package/src/pin/tickets.mjs +697 -0
  56. package/src/pin/toolbar.js +3181 -0
  57. package/src/shell/Browse.astro +371 -0
  58. package/src/shell/CardGrid.astro +297 -0
  59. package/src/shell/Viewport.astro +1330 -0
  60. package/src/shell/index.json.ts +12 -0
  61. package/src/shell/lab-index.ts +344 -0
  62. package/src/shell/lab-params.ts +245 -0
  63. package/src/shell/live-files.mjs +164 -0
  64. package/src/shell/marks.mjs +136 -0
  65. package/src/types/index.ts +6 -0
  66. package/src/types/types.ts +239 -0
  67. package/src/types/virtual.d.ts +29 -0
  68. package/src/ui/components/app.astro +13 -0
  69. package/src/ui/components/build-path.ts +13 -0
  70. package/src/ui/components/build-tree.ts +108 -0
  71. package/src/ui/components/collapse-duration.ts +28 -0
  72. package/src/ui/components/compress-terms.ts +10 -0
  73. package/src/ui/components/dashboard-layout.astro +39 -0
  74. package/src/ui/components/home.astro +65 -0
  75. package/src/ui/components/layout.astro +110 -0
  76. package/src/ui/components/preview-layout.astro +109 -0
  77. package/src/ui/components/sidebar-button-fullscreen.astro +38 -0
  78. package/src/ui/components/sidebar-button-search.astro +23 -0
  79. package/src/ui/components/sidebar-button-theme.astro +9 -0
  80. package/src/ui/components/sidebar-button.astro +24 -0
  81. package/src/ui/components/sidebar-resize-handle.astro +74 -0
  82. package/src/ui/components/sidebar-search-panel.astro +41 -0
  83. package/src/ui/components/sidebar-search-script.ts +103 -0
  84. package/src/ui/components/sidebar-title.astro +17 -0
  85. package/src/ui/components/sidebar-tree-node.astro +143 -0
  86. package/src/ui/components/sidebar-tree.astro +84 -0
  87. package/src/ui/components/sidebar.astro +29 -0
  88. package/src/ui/components/theme-message.ts +26 -0
  89. package/src/ui/components/theme-script.astro +71 -0
  90. package/src/ui/components/theme-toggle.astro +63 -0
  91. package/src/ui/components/theme.ts +32 -0
  92. package/src/ui/index.ts +4 -0
  93. package/src/ui/lab.css +549 -0
  94. package/virtual.d.ts +42 -0
@@ -0,0 +1,130 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import colors from "yoctocolors";
5
+ import { APP_PAGE } from "./lib-paths.js";
6
+ import { resolveOptions } from "./options.js";
7
+ import { createPathBuilder } from "./utils/path-builder.js";
8
+ import {
9
+ createVirtualRouteComponent,
10
+ getVirtualRoutes
11
+ } from "./virtual-module/virtual-routes.js";
12
+ import { createVirtualFilesPlugin } from "./virtual-module/vite-plugin.js";
13
+ function createAstrobookIntegration(userOptions) {
14
+ let astrobookBaseForLogging;
15
+ const resolvedOptions = resolveOptions(userOptions);
16
+ return {
17
+ name: "astrobook",
18
+ hooks: {
19
+ "astro:config:setup": async ({
20
+ updateConfig,
21
+ injectRoute,
22
+ createCodegenDir,
23
+ config,
24
+ logger,
25
+ command
26
+ }) => {
27
+ const rootDir = path.resolve(
28
+ fileURLToPath(config.root),
29
+ resolvedOptions.directory
30
+ );
31
+ const astroBase = config.base || "";
32
+ const astrobookSubpath = resolvedOptions.subpath;
33
+ const buildPath = createPathBuilder({
34
+ trailingSlash: config.trailingSlash
35
+ });
36
+ const dashboardSubpath = buildPath(
37
+ astrobookSubpath,
38
+ resolvedOptions.dashboardSubpath
39
+ );
40
+ const previewSubpath = buildPath(
41
+ astrobookSubpath,
42
+ resolvedOptions.previewSubpath
43
+ );
44
+ if (dashboardSubpath === previewSubpath) {
45
+ throw new Error(
46
+ "[Astrobook] The dashboard and preview subpaths cannot be the same. Please set different values for `dashboardSubpath` and `previewSubpath` options."
47
+ );
48
+ }
49
+ const astrobookBase = buildPath(astroBase, astrobookSubpath);
50
+ const dashboardBase = buildPath(astroBase, dashboardSubpath);
51
+ const storyBase = buildPath(astroBase, previewSubpath);
52
+ if (astrobookSubpath && command === "dev") {
53
+ astrobookBaseForLogging = astrobookBase;
54
+ } else {
55
+ astrobookBaseForLogging = void 0;
56
+ }
57
+ logger.debug(`Creating dedicated folder`);
58
+ const codegenDir = fileURLToPath(createCodegenDir());
59
+ logger.debug(`Created dedicated folder at ${codegenDir}`);
60
+ logger.debug(`Scanning for stories in ${rootDir}`);
61
+ const routes = await getVirtualRoutes(
62
+ rootDir,
63
+ codegenDir,
64
+ logger,
65
+ dashboardSubpath,
66
+ previewSubpath,
67
+ buildPath
68
+ );
69
+ logger.debug(`Writing files to ${codegenDir}`);
70
+ await Promise.all(
71
+ Array.from(routes.values(), async (route) => {
72
+ const filePath = route.entrypoint;
73
+ const fileContent = createVirtualRouteComponent(route);
74
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
75
+ await fs.writeFile(filePath, fileContent, { encoding: "utf-8" });
76
+ })
77
+ );
78
+ updateConfig({
79
+ vite: {
80
+ plugins: [
81
+ createVirtualFilesPlugin(
82
+ rootDir,
83
+ {
84
+ astrobookBase,
85
+ dashboardBase,
86
+ storyBase,
87
+ head: resolvedOptions.head,
88
+ home: resolvedOptions.home,
89
+ homeContent: resolvedOptions.homeContent,
90
+ css: resolvedOptions.css,
91
+ title: resolvedOptions.title,
92
+ trailingSlash: config.trailingSlash
93
+ },
94
+ config,
95
+ logger
96
+ )
97
+ ]
98
+ }
99
+ });
100
+ logger.debug(`Injecting routes`);
101
+ for (const route of routes.values()) {
102
+ injectRoute({
103
+ pattern: route.pattern,
104
+ entrypoint: route.entrypoint,
105
+ prerender: true
106
+ });
107
+ }
108
+ injectRoute({
109
+ pattern: astrobookSubpath,
110
+ entrypoint: APP_PAGE,
111
+ prerender: true
112
+ });
113
+ },
114
+ "astro:server:start": ({ logger, address }) => {
115
+ if (astrobookBaseForLogging) {
116
+ const url = new URL(
117
+ astrobookBaseForLogging,
118
+ `http://localhost:${address.port}`
119
+ );
120
+ const message = colors.bgGreen(colors.white(colors.bold(" astrobook "))) + " is available at " + colors.cyan(url.href);
121
+ const plainLogger = logger.fork("SKIP_FORMAT");
122
+ plainLogger.info(message);
123
+ }
124
+ }
125
+ }
126
+ };
127
+ }
128
+ export {
129
+ createAstrobookIntegration
130
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ createAstrobookIntegration
3
+ } from "./astro-integration.js";
4
+ export {
5
+ createAstrobookIntegration
6
+ };
@@ -0,0 +1,29 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import slash from "slash";
3
+ const LIB_BASE = new URL(
4
+ import.meta.url.endsWith("/dist/core/lib-paths.js") ? "../../src/core/" : "./",
5
+ import.meta.url
6
+ );
7
+ function libFile(relative) {
8
+ return slash(fileURLToPath(new URL(relative, LIB_BASE)));
9
+ }
10
+ const CLIENT_MODULE = libFile("./client.ts");
11
+ const STORY_PAGE = libFile("./lib/pages/story.astro");
12
+ const PREVIEW_PAGE = libFile("./lib/pages/preview.astro");
13
+ const APP_PAGE = libFile("./lib/pages/app.astro");
14
+ const WITH_DECORATORS_COMPONENT = libFile(
15
+ "./lib/components/with-decorators.astro"
16
+ );
17
+ const HEAD_COMPONENT = libFile("./lib/components/head.astro");
18
+ const HOME_COMPONENT = libFile("./lib/components/home.astro");
19
+ const EMPTY_COMPONENT = libFile("./lib/components/empty.astro");
20
+ export {
21
+ APP_PAGE,
22
+ CLIENT_MODULE,
23
+ EMPTY_COMPONENT,
24
+ HEAD_COMPONENT,
25
+ HOME_COMPONENT,
26
+ PREVIEW_PAGE,
27
+ STORY_PAGE,
28
+ WITH_DECORATORS_COMPONENT
29
+ };
@@ -0,0 +1,173 @@
1
+ import {
2
+ EMPTY_COMPONENT,
3
+ HEAD_COMPONENT,
4
+ HOME_COMPONENT
5
+ } from "./lib-paths.js";
6
+ const ASTROBOOK_VERSION = "0.13.3";
7
+ const DEFAULT_VERSION_HREF = "https://github.com/ocavue/astrobook/blob/master/packages/astrobook/CHANGELOG.md";
8
+ const DEFAULT_REPO_HREF = "https://github.com/ocavue/astrobook";
9
+ function stringify(value) {
10
+ switch (typeof value) {
11
+ case "string":
12
+ return `"${value}"`;
13
+ case "number":
14
+ case "bigint":
15
+ case "boolean":
16
+ return String(value);
17
+ case "symbol":
18
+ case "function":
19
+ return value.toString();
20
+ case "object":
21
+ return value === null ? "null" : Array.isArray(value) ? "Array" : "Object";
22
+ default:
23
+ return "undefined";
24
+ }
25
+ }
26
+ function typeIssue(expected, value, path) {
27
+ return {
28
+ message: `Invalid type: Expected ${expected} but received ${stringify(value)}`,
29
+ path
30
+ };
31
+ }
32
+ function readString(value, fallback, path, issues) {
33
+ if (value === void 0) return fallback;
34
+ if (typeof value !== "string") {
35
+ issues.push(typeIssue("string", value, path));
36
+ return fallback;
37
+ }
38
+ return value;
39
+ }
40
+ function readStringArray(value, path, issues) {
41
+ if (value === void 0) return [];
42
+ if (!Array.isArray(value)) {
43
+ issues.push(typeIssue("Array", value, path));
44
+ return [];
45
+ }
46
+ const result = [];
47
+ value.forEach((item, index) => {
48
+ if (typeof item !== "string") {
49
+ issues.push(typeIssue("string", item, `${path}.${index}`));
50
+ return;
51
+ }
52
+ result.push(item);
53
+ });
54
+ return result;
55
+ }
56
+ function readStringOrFalse(value, fallback, path, issues) {
57
+ if (value === void 0) return fallback;
58
+ if (value === false) return false;
59
+ if (typeof value !== "string") {
60
+ issues.push(typeIssue("(string | false)", value, path));
61
+ return fallback;
62
+ }
63
+ return value;
64
+ }
65
+ function readBadge(value, defaults, path, issues) {
66
+ if (value === void 0) return { ...defaults };
67
+ if (value === false) return false;
68
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
69
+ issues.push(typeIssue("(Object | false)", value, path));
70
+ return { ...defaults };
71
+ }
72
+ const badge = value;
73
+ return {
74
+ href: readString(badge["href"], defaults.href, `${path}.href`, issues),
75
+ label: readString(badge["label"], defaults.label, `${path}.label`, issues)
76
+ };
77
+ }
78
+ function readHomeContent(value, issues) {
79
+ let content = {};
80
+ if (value !== void 0) {
81
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
82
+ issues.push(typeIssue("Object", value, "homeContent"));
83
+ } else {
84
+ content = value;
85
+ }
86
+ }
87
+ return {
88
+ title: readStringOrFalse(
89
+ content["title"],
90
+ "Astrobook",
91
+ "homeContent.title",
92
+ issues
93
+ ),
94
+ subtitle: readStringOrFalse(
95
+ content["subtitle"],
96
+ "The minimal UI component playground",
97
+ "homeContent.subtitle",
98
+ issues
99
+ ),
100
+ version: readBadge(
101
+ content["version"],
102
+ { href: DEFAULT_VERSION_HREF, label: `v${ASTROBOOK_VERSION}` },
103
+ "homeContent.version",
104
+ issues
105
+ ),
106
+ repo: readBadge(
107
+ content["repo"],
108
+ { href: DEFAULT_REPO_HREF, label: "Star on GitHub" },
109
+ "homeContent.repo",
110
+ issues
111
+ )
112
+ };
113
+ }
114
+ function readHome(value, issues) {
115
+ if (value === void 0) return HOME_COMPONENT;
116
+ if (value === false) return EMPTY_COMPONENT;
117
+ if (typeof value !== "string") {
118
+ issues.push(typeIssue("(string | false)", value, "home"));
119
+ return HOME_COMPONENT;
120
+ }
121
+ if (value.length === 0) {
122
+ issues.push({
123
+ message: "Invalid length: Expected >=1 but received 0",
124
+ path: "home"
125
+ });
126
+ return HOME_COMPONENT;
127
+ }
128
+ return value;
129
+ }
130
+ function resolveOptions(options) {
131
+ const issues = [];
132
+ let input = {};
133
+ if (options !== void 0) {
134
+ if (typeof options !== "object" || options === null) {
135
+ issues.push(typeIssue("Object", options, ""));
136
+ } else {
137
+ input = options;
138
+ }
139
+ }
140
+ const resolved = {
141
+ directory: readString(input["directory"], ".", "directory", issues),
142
+ subpath: readString(input["subpath"], "", "subpath", issues),
143
+ dashboardSubpath: readString(
144
+ input["dashboardSubpath"],
145
+ "dashboard",
146
+ "dashboardSubpath",
147
+ issues
148
+ ),
149
+ previewSubpath: readString(
150
+ input["previewSubpath"],
151
+ "stories",
152
+ "previewSubpath",
153
+ issues
154
+ ),
155
+ title: readString(input["title"], "Astrobook", "title", issues),
156
+ css: readStringArray(input["css"], "css", issues),
157
+ head: readString(input["head"], HEAD_COMPONENT, "head", issues),
158
+ home: readHome(input["home"], issues),
159
+ homeContent: readHomeContent(input["homeContent"], issues)
160
+ };
161
+ if (issues.length > 0) {
162
+ const summary = issues.map(
163
+ (issue) => issue.path ? `\xD7 ${issue.message}
164
+ \u2192 at ${issue.path}` : `\xD7 ${issue.message}`
165
+ ).join("\n");
166
+ throw new Error(`Invalid Astrobook options:
167
+ ${summary}`);
168
+ }
169
+ return resolved;
170
+ }
171
+ export {
172
+ resolveOptions
173
+ };
@@ -0,0 +1,52 @@
1
+ import { tsPlugin } from "@sveltejs/acorn-typescript";
2
+ import { Parser } from "acorn";
3
+ function getExports(code) {
4
+ const parser = Parser.extend(
5
+ tsPlugin({
6
+ dts: false,
7
+ jsx: {
8
+ allowNamespaces: true,
9
+ allowNamespacedObjects: true
10
+ }
11
+ })
12
+ );
13
+ const ast = parser.parse(code, {
14
+ sourceType: "module",
15
+ ecmaVersion: "latest",
16
+ allowImportExportEverywhere: true,
17
+ locations: true
18
+ });
19
+ const exports = /* @__PURE__ */ new Set();
20
+ for (const node of ast.body) {
21
+ if (node.type === "ExportNamedDeclaration") {
22
+ for (const specifier of node.specifiers) {
23
+ const { exported } = specifier;
24
+ if (exported.type === "Identifier") {
25
+ exports.add(exported.name);
26
+ }
27
+ }
28
+ const { declaration } = node;
29
+ if (declaration?.type === "VariableDeclaration") {
30
+ const { declarations } = declaration;
31
+ for (const declaration2 of declarations) {
32
+ const id = declaration2.id;
33
+ if (id.type === "Identifier") {
34
+ exports.add(id.name);
35
+ }
36
+ }
37
+ }
38
+ if (declaration?.type === "FunctionDeclaration") {
39
+ exports.add(declaration.id.name);
40
+ }
41
+ if (declaration?.type === "ClassDeclaration") {
42
+ exports.add(declaration.id.name);
43
+ }
44
+ } else if (node.type === "ExportDefaultDeclaration") {
45
+ exports.add("default");
46
+ }
47
+ }
48
+ return Array.from(exports).sort();
49
+ }
50
+ export {
51
+ getExports
52
+ };
@@ -0,0 +1,12 @@
1
+ import assert from "node:assert";
2
+ function invariant(condition, message = "Unexpected invariant violation") {
3
+ if (!condition) {
4
+ assert(
5
+ condition,
6
+ `[astrobook] Unexpected internal error. Please open an issue at https://github.com/ocavue/astrobook/issues. ${message}`
7
+ );
8
+ }
9
+ }
10
+ export {
11
+ invariant
12
+ };
@@ -0,0 +1,15 @@
1
+ const wordSeparators = /[\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]+/;
2
+ const capital_plus_lower = /[A-ZÀ-Ý\u00C0-\u00D6\u00D9-\u00DD][a-zà-ÿ]/g;
3
+ const capitals = /[A-ZÀ-Ý\u00C0-\u00D6\u00D9-\u00DD]+/g;
4
+ function kebabCase(str) {
5
+ str = str.replace(capital_plus_lower, function(match) {
6
+ return " " + (match[0].toLowerCase() || match[0]) + match[1];
7
+ });
8
+ str = str.replace(capitals, function(match) {
9
+ return " " + match.toLowerCase();
10
+ });
11
+ return str.trim().split(wordSeparators).join("-").replace(/^-/, "").replace(/-\s*$/, "");
12
+ }
13
+ export {
14
+ kebabCase as default
15
+ };
@@ -0,0 +1,22 @@
1
+ import {
2
+ ensureLeadingSlash,
3
+ ensureTrailingSlash,
4
+ stripLeadingAndTrailingSlashes,
5
+ stripTrailingSlash
6
+ } from "./path.js";
7
+ function createPathBuilder({
8
+ trailingSlash = "ignore"
9
+ }) {
10
+ return function buildPath(...parts) {
11
+ let result = parts.map(stripLeadingAndTrailingSlashes).filter(Boolean).join("/");
12
+ if (trailingSlash === "always") {
13
+ result = ensureTrailingSlash(result);
14
+ } else if (trailingSlash === "never") {
15
+ result = stripTrailingSlash(result);
16
+ }
17
+ return ensureLeadingSlash(result);
18
+ };
19
+ }
20
+ export {
21
+ createPathBuilder
22
+ };
@@ -0,0 +1,53 @@
1
+ function ensureLeadingSlash(href) {
2
+ if (href[0] !== "/") href = "/" + href;
3
+ return href;
4
+ }
5
+ function ensureTrailingSlash(href) {
6
+ if (href[href.length - 1] !== "/") href += "/";
7
+ return href;
8
+ }
9
+ function ensureLeadingAndTrailingSlashes(href) {
10
+ href = ensureLeadingSlash(href);
11
+ href = ensureTrailingSlash(href);
12
+ return href;
13
+ }
14
+ function stripLeadingSlash(href) {
15
+ while (href[0] === "/") href = href.slice(1);
16
+ return href;
17
+ }
18
+ function stripTrailingSlash(href) {
19
+ while (href[href.length - 1] === "/") href = href.slice(0, -1);
20
+ return href;
21
+ }
22
+ function stripLeadingAndTrailingSlashes(href) {
23
+ href = stripLeadingSlash(href);
24
+ href = stripTrailingSlash(href);
25
+ return href;
26
+ }
27
+ function stripHtmlExtension(path) {
28
+ const pathWithoutTrailingSlash = stripTrailingSlash(path);
29
+ return pathWithoutTrailingSlash.endsWith(".html") ? pathWithoutTrailingSlash.slice(0, -5) : path;
30
+ }
31
+ function ensureHtmlExtension(path) {
32
+ path = stripLeadingAndTrailingSlashes(path);
33
+ if (!path.endsWith(".html")) {
34
+ path = path ? path + ".html" : "/index.html";
35
+ }
36
+ return ensureLeadingSlash(path);
37
+ }
38
+ function stripExtension(path) {
39
+ const periodIndex = path.lastIndexOf(".");
40
+ return path.slice(0, periodIndex > -1 ? periodIndex : void 0);
41
+ }
42
+ export {
43
+ ensureHtmlExtension,
44
+ ensureLeadingAndTrailingSlashes,
45
+ ensureLeadingSlash,
46
+ ensureTrailingSlash,
47
+ stripExtension,
48
+ stripHtmlExtension,
49
+ stripLeadingAndTrailingSlashes,
50
+ stripLeadingSlash,
51
+ stripLeadingAndTrailingSlashes as stripSlashes,
52
+ stripTrailingSlash
53
+ };
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fdir } from "fdir";
4
+ import slash from "slash";
5
+ import { getExports } from "../utils/get-exports.js";
6
+ import { invariant } from "../utils/invariant.js";
7
+ import kebabCase from "../utils/kebab-case.js";
8
+ async function listStoryFiles(rootDir) {
9
+ invariant(
10
+ path.isAbsolute(rootDir),
11
+ `Root directory must be absolute, but got '${rootDir}'`
12
+ );
13
+ const filePaths = await new fdir().withSymlinks().withFullPaths().normalize().glob("./**/*.stories.{ts,tsx,js,jsx,mts,mtsx,mjs,mjsx}").exclude((dirname) => dirname === "node_modules").crawl(rootDir).withPromise();
14
+ return filePaths.sort();
15
+ }
16
+ async function parseStoryFiles(filePath) {
17
+ const code = await fs.readFile(filePath, "utf-8");
18
+ const exports = getExports(code);
19
+ const defaultExport = exports.includes("default");
20
+ const namedExports = exports.filter((name) => name !== "default");
21
+ return { filePath, defaultExport, namedExports };
22
+ }
23
+ function convertStoryFileToModule(rootDir, file) {
24
+ if (file.namedExports.length === 0) {
25
+ console.warn(`[astrobook] File ${file.filePath} has no named exports`);
26
+ return;
27
+ }
28
+ if (!file.defaultExport) {
29
+ console.warn(`[astrobook] File ${file.filePath} has no default export`);
30
+ return;
31
+ }
32
+ const relativePath = path.normalize(path.relative(rootDir, file.filePath));
33
+ const relativePathWithoutStories = relativePath.replace(
34
+ /\.stories\.\w+$/i,
35
+ ""
36
+ );
37
+ const parts = relativePathWithoutStories.split(path.sep);
38
+ const directory = parts.slice(0, -1).join("/");
39
+ const name = parts.pop();
40
+ invariant(name, `Unexpected file path: ${file.filePath}`);
41
+ const moduleId = directory ? `${directory}/${kebabCase(name)}` : kebabCase(name);
42
+ return {
43
+ id: moduleId,
44
+ name,
45
+ directory,
46
+ importPath: slash(file.filePath),
47
+ stories: file.namedExports.map((name2) => {
48
+ return { id: `${moduleId}/${kebabCase(name2)}`, name: name2 };
49
+ })
50
+ };
51
+ }
52
+ async function getStoryModules(rootDir) {
53
+ const storyFilePaths = await listStoryFiles(rootDir);
54
+ const storyFiles = await Promise.all(storyFilePaths.map(parseStoryFiles));
55
+ return storyFiles.map((storyFile) => convertStoryFileToModule(rootDir, storyFile)).filter((module) => !!module);
56
+ }
57
+ export {
58
+ convertStoryFileToModule,
59
+ getStoryModules
60
+ };
@@ -0,0 +1,8 @@
1
+ import { getStoryModules } from "./get-story-modules.js";
2
+ async function loadStoryModules(rootDir) {
3
+ const modules = await getStoryModules(rootDir);
4
+ return `export default ${JSON.stringify(modules)}`;
5
+ }
6
+ export {
7
+ loadStoryModules
8
+ };
@@ -0,0 +1,22 @@
1
+ const STORY_MODULES_ID = "virtual:astrobook/story-modules.mjs";
2
+ const STORY_MODULES_RESOLVED_ID = "__virtual_astrobook_story_modules__.mjs";
3
+ const GLOBAL_CONFIG_ID = "virtual:astrobook/global-config.mjs";
4
+ const GLOBAL_CONFIG_RESOLVED_ID = "__virtual_astrobook_user_config__.mjs";
5
+ const COMPONENT_HEAD_ID = "virtual:astrobook/components/head.mjs";
6
+ const COMPONENT_HEAD_RESOLVED_ID = "__virtual_astrobook_COMPONENT_HEAD__.mjs";
7
+ const COMPONENT_HOME_ID = "virtual:astrobook/components/home.mjs";
8
+ const COMPONENT_HOME_RESOLVED_ID = "__virtual_astrobook_COMPONENT_HOME__.mjs";
9
+ const USER_CSS_ID = "virtual:astrobook/user-css.mjs";
10
+ const USER_CSS_RESOLVED_ID = "__virtual_astrobook_user_css__.mjs";
11
+ export {
12
+ COMPONENT_HEAD_ID,
13
+ COMPONENT_HEAD_RESOLVED_ID,
14
+ COMPONENT_HOME_ID,
15
+ COMPONENT_HOME_RESOLVED_ID,
16
+ GLOBAL_CONFIG_ID,
17
+ GLOBAL_CONFIG_RESOLVED_ID,
18
+ STORY_MODULES_ID,
19
+ STORY_MODULES_RESOLVED_ID,
20
+ USER_CSS_ID,
21
+ USER_CSS_RESOLVED_ID
22
+ };
@@ -0,0 +1,83 @@
1
+ import path from "node:path";
2
+ import slash from "slash";
3
+ import {
4
+ CLIENT_MODULE,
5
+ PREVIEW_PAGE,
6
+ STORY_PAGE,
7
+ WITH_DECORATORS_COMPONENT
8
+ } from "../lib-paths.js";
9
+ import { invariant } from "../utils/invariant.js";
10
+ import { getStoryModules } from "./get-story-modules.js";
11
+ async function getVirtualRoutes(rootDir, codegenDir, logger, dashboardSubpath, previewSubpath, buildPath) {
12
+ const routes = [];
13
+ const storyModules = await getStoryModules(rootDir);
14
+ for (const storyModule of storyModules) {
15
+ logger.debug(
16
+ `Found ${storyModule.stories.length} stories in ${storyModule.importPath}`
17
+ );
18
+ for (const story of storyModule.stories) {
19
+ invariant(
20
+ !story.id.startsWith(".."),
21
+ `Story ID cannot start with '..', but got '${story.id}'`
22
+ );
23
+ invariant(
24
+ !story.id.startsWith("/"),
25
+ `Story ID cannot start with '/', but got '${story.id}'`
26
+ );
27
+ routes.push(
28
+ {
29
+ pattern: buildPath(dashboardSubpath, story.id),
30
+ entrypoint: slash(
31
+ path.resolve(codegenDir, "dashboard", story.id + ".astro")
32
+ ),
33
+ storyModule,
34
+ story,
35
+ props: { hasSidebar: true, story: story.id }
36
+ },
37
+ {
38
+ pattern: buildPath(previewSubpath, story.id),
39
+ entrypoint: slash(
40
+ path.resolve(codegenDir, "stories", story.id + ".astro")
41
+ ),
42
+ storyModule,
43
+ story,
44
+ props: { hasSidebar: false, story: story.id }
45
+ }
46
+ );
47
+ }
48
+ }
49
+ logger.info(`Found ${routes.length} stories in ${storyModules.length} files`);
50
+ return new Map(routes.map((route) => [route.pattern, route]));
51
+ }
52
+ function createVirtualRouteComponent(route) {
53
+ const modName = `astrobook_module_${route.storyModule.id}`.replaceAll(/\W+/g, "_").replaceAll(/_+/g, "_").replace(/_+$/, "");
54
+ const page = route.props.hasSidebar ? STORY_PAGE : PREVIEW_PAGE;
55
+ return `
56
+ ---
57
+ // Automatically generated by Astrobook
58
+
59
+ import StoryPage from '${page}';
60
+ import WithDecorators from '${WITH_DECORATORS_COMPONENT}';
61
+ import { parseStoryDefaultExport, parseStoryNamedExport } from '${CLIENT_MODULE}';
62
+
63
+ import * as ${modName} from '${route.storyModule.importPath}';
64
+
65
+ const { isAstroComponent } = parseStoryDefaultExport(${modName}, '${route.storyModule.importPath}');
66
+ const { decorators, args } = parseStoryNamedExport(${modName}, '${route.storyModule.importPath}', '${route.story.name}');
67
+ ---
68
+
69
+ <StoryPage story={'${route.props.story}'} hasSidebar={${route.props.hasSidebar}}>
70
+ <WithDecorators decorators={decorators}>
71
+ {isAstroComponent ? (
72
+ <${modName}.default.component {...args} />
73
+ ) : (
74
+ <${modName}.default.component {...args} client:load />
75
+ )}
76
+ </WithDecorators>
77
+ </StoryPage>
78
+ `.trim();
79
+ }
80
+ export {
81
+ createVirtualRouteComponent,
82
+ getVirtualRoutes
83
+ };