@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,262 @@
1
+ // Vendored from astrobook 0.13.3 (options.ts) — see ./LICENSE-astrobook.
2
+ //
3
+ // Divergence: rewritten in plain TypeScript. Upstream declares these options as a schema for a
4
+ // validation library that weighs 1.8 MB and has this one caller (named in LICENSE-astrobook
5
+ // § divergence 4), so the schema is unrolled here by hand. Every option name, every default, the
6
+ // `home: false` → empty-component transform and the resolved shape are unchanged, and the thrown
7
+ // message keeps upstream's layout so a mistake in a site's astro.config reads the same:
8
+ //
9
+ // Invalid Astrobook options:
10
+ // × Invalid type: Expected string but received 111
11
+ // → at directory
12
+ //
13
+ // Divergence: the three component defaults are absolute paths into ./lib-paths.ts rather than bare
14
+ // `astrobook/...` ids — see that file for why.
15
+ import type { IntegrationOptions } from '../types/types.ts'
16
+
17
+ import {
18
+ EMPTY_COMPONENT,
19
+ HEAD_COMPONENT,
20
+ HOME_COMPONENT,
21
+ } from './lib-paths.ts'
22
+
23
+ /**
24
+ * The upstream Astrobook version, inlined.
25
+ *
26
+ * It is the default label of the home page's version badge, whose default href is Astrobook's own
27
+ * CHANGELOG — so this package's version would be the wrong number to print beside it. It is only
28
+ * ever rendered by Astrobook's built-in home, which the lab replaces with its own.
29
+ */
30
+ const ASTROBOOK_VERSION = '0.13.3'
31
+
32
+ const DEFAULT_VERSION_HREF =
33
+ 'https://github.com/ocavue/astrobook/blob/master/packages/astrobook/CHANGELOG.md'
34
+ const DEFAULT_REPO_HREF = 'https://github.com/ocavue/astrobook'
35
+
36
+ export interface ResolvedOptions {
37
+ directory: string
38
+ subpath: string
39
+ dashboardSubpath: string
40
+ previewSubpath: string
41
+ title: string
42
+ css: string[]
43
+ head: string
44
+ home: string
45
+ homeContent: {
46
+ title: string | false
47
+ subtitle: string | false
48
+ version: { href: string; label: string } | false
49
+ repo: { href: string; label: string } | false
50
+ }
51
+ }
52
+
53
+ /** One collected problem, in the shape upstream's validator summarised them. */
54
+ interface Issue {
55
+ message: string
56
+ path: string
57
+ }
58
+
59
+ /** How upstream's validator rendered the value it received, so the message reads the same. */
60
+ function stringify(value: unknown): string {
61
+ switch (typeof value) {
62
+ case 'string':
63
+ return `"${value}"`
64
+ case 'number':
65
+ case 'bigint':
66
+ case 'boolean':
67
+ return String(value)
68
+ case 'symbol':
69
+ case 'function':
70
+ return value.toString()
71
+ case 'object':
72
+ return value === null ? 'null' : Array.isArray(value) ? 'Array' : 'Object'
73
+ default:
74
+ return 'undefined'
75
+ }
76
+ }
77
+
78
+ function typeIssue(expected: string, value: unknown, path: string): Issue {
79
+ return {
80
+ message: `Invalid type: Expected ${expected} but received ${stringify(value)}`,
81
+ path,
82
+ }
83
+ }
84
+
85
+ function readString(
86
+ value: unknown,
87
+ fallback: string,
88
+ path: string,
89
+ issues: Issue[],
90
+ ): string {
91
+ if (value === undefined) return fallback
92
+ if (typeof value !== 'string') {
93
+ issues.push(typeIssue('string', value, path))
94
+ return fallback
95
+ }
96
+ return value
97
+ }
98
+
99
+ function readStringArray(
100
+ value: unknown,
101
+ path: string,
102
+ issues: Issue[],
103
+ ): string[] {
104
+ if (value === undefined) return []
105
+ if (!Array.isArray(value)) {
106
+ issues.push(typeIssue('Array', value, path))
107
+ return []
108
+ }
109
+ const result: string[] = []
110
+ value.forEach((item, index) => {
111
+ if (typeof item !== 'string') {
112
+ issues.push(typeIssue('string', item, `${path}.${index}`))
113
+ return
114
+ }
115
+ result.push(item)
116
+ })
117
+ return result
118
+ }
119
+
120
+ /** A `string | false` field of `homeContent`. */
121
+ function readStringOrFalse(
122
+ value: unknown,
123
+ fallback: string,
124
+ path: string,
125
+ issues: Issue[],
126
+ ): string | false {
127
+ if (value === undefined) return fallback
128
+ if (value === false) return false
129
+ if (typeof value !== 'string') {
130
+ issues.push(typeIssue('(string | false)', value, path))
131
+ return fallback
132
+ }
133
+ return value
134
+ }
135
+
136
+ /** A `{ href, label } | false` badge of `homeContent`. */
137
+ function readBadge(
138
+ value: unknown,
139
+ defaults: { href: string; label: string },
140
+ path: string,
141
+ issues: Issue[],
142
+ ): { href: string; label: string } | false {
143
+ if (value === undefined) return { ...defaults }
144
+ if (value === false) return false
145
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
146
+ issues.push(typeIssue('(Object | false)', value, path))
147
+ return { ...defaults }
148
+ }
149
+ const badge = value as Record<string, unknown>
150
+ return {
151
+ href: readString(badge['href'], defaults.href, `${path}.href`, issues),
152
+ label: readString(badge['label'], defaults.label, `${path}.label`, issues),
153
+ }
154
+ }
155
+
156
+ function readHomeContent(
157
+ value: unknown,
158
+ issues: Issue[],
159
+ ): ResolvedOptions['homeContent'] {
160
+ let content: Record<string, unknown> = {}
161
+ if (value !== undefined) {
162
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
163
+ issues.push(typeIssue('Object', value, 'homeContent'))
164
+ } else {
165
+ content = value as Record<string, unknown>
166
+ }
167
+ }
168
+
169
+ return {
170
+ title: readStringOrFalse(
171
+ content['title'],
172
+ 'Astrobook',
173
+ 'homeContent.title',
174
+ issues,
175
+ ),
176
+ subtitle: readStringOrFalse(
177
+ content['subtitle'],
178
+ 'The minimal UI component playground',
179
+ 'homeContent.subtitle',
180
+ issues,
181
+ ),
182
+ version: readBadge(
183
+ content['version'],
184
+ { href: DEFAULT_VERSION_HREF, label: `v${ASTROBOOK_VERSION}` },
185
+ 'homeContent.version',
186
+ issues,
187
+ ),
188
+ repo: readBadge(
189
+ content['repo'],
190
+ { href: DEFAULT_REPO_HREF, label: 'Star on GitHub' },
191
+ 'homeContent.repo',
192
+ issues,
193
+ ),
194
+ }
195
+ }
196
+
197
+ /**
198
+ * `home` takes a component path, or `false` for a home page that renders nothing. Upstream folds
199
+ * the `false` case into the resolved path here so nothing downstream has to know about it.
200
+ */
201
+ function readHome(value: unknown, issues: Issue[]): string {
202
+ if (value === undefined) return HOME_COMPONENT
203
+ if (value === false) return EMPTY_COMPONENT
204
+ if (typeof value !== 'string') {
205
+ issues.push(typeIssue('(string | false)', value, 'home'))
206
+ return HOME_COMPONENT
207
+ }
208
+ if (value.length === 0) {
209
+ issues.push({
210
+ message: 'Invalid length: Expected >=1 but received 0',
211
+ path: 'home',
212
+ })
213
+ return HOME_COMPONENT
214
+ }
215
+ return value
216
+ }
217
+
218
+ export function resolveOptions(options?: IntegrationOptions): ResolvedOptions {
219
+ const issues: Issue[] = []
220
+
221
+ let input: Record<string, unknown> = {}
222
+ if (options !== undefined) {
223
+ if (typeof options !== 'object' || options === null) {
224
+ issues.push(typeIssue('Object', options, ''))
225
+ } else {
226
+ input = options as unknown as Record<string, unknown>
227
+ }
228
+ }
229
+
230
+ const resolved: ResolvedOptions = {
231
+ directory: readString(input['directory'], '.', 'directory', issues),
232
+ subpath: readString(input['subpath'], '', 'subpath', issues),
233
+ dashboardSubpath: readString(
234
+ input['dashboardSubpath'],
235
+ 'dashboard',
236
+ 'dashboardSubpath',
237
+ issues,
238
+ ),
239
+ previewSubpath: readString(
240
+ input['previewSubpath'],
241
+ 'stories',
242
+ 'previewSubpath',
243
+ issues,
244
+ ),
245
+ title: readString(input['title'], 'Astrobook', 'title', issues),
246
+ css: readStringArray(input['css'], 'css', issues),
247
+ head: readString(input['head'], HEAD_COMPONENT, 'head', issues),
248
+ home: readHome(input['home'], issues),
249
+ homeContent: readHomeContent(input['homeContent'], issues),
250
+ }
251
+
252
+ if (issues.length > 0) {
253
+ const summary = issues
254
+ .map((issue) =>
255
+ issue.path ? `× ${issue.message}\n → at ${issue.path}` : `× ${issue.message}`,
256
+ )
257
+ .join('\n')
258
+ throw new Error(`Invalid Astrobook options:\n${summary}`)
259
+ }
260
+
261
+ return resolved
262
+ }
@@ -0,0 +1,59 @@
1
+ import { tsPlugin } from '@sveltejs/acorn-typescript'
2
+ import { Parser } from 'acorn'
3
+
4
+ /**
5
+ * Parses the content of the given file and returns all its exports
6
+ */
7
+ export function getExports(code: string): string[] {
8
+ // Parse the code into an AST
9
+ const parser = Parser.extend(
10
+ tsPlugin({
11
+ dts: false,
12
+ jsx: {
13
+ allowNamespaces: true,
14
+ allowNamespacedObjects: true,
15
+ },
16
+ }),
17
+ )
18
+ const ast = parser.parse(code, {
19
+ sourceType: 'module',
20
+ ecmaVersion: 'latest',
21
+ allowImportExportEverywhere: true,
22
+ locations: true,
23
+ })
24
+
25
+ const exports = new Set<string>()
26
+
27
+ // Walk through the AST
28
+ for (const node of ast.body) {
29
+ if (node.type === 'ExportNamedDeclaration') {
30
+ for (const specifier of node.specifiers) {
31
+ const { exported } = specifier
32
+ if (exported.type === 'Identifier') {
33
+ exports.add(exported.name)
34
+ }
35
+ }
36
+
37
+ const { declaration } = node
38
+ if (declaration?.type === 'VariableDeclaration') {
39
+ const { declarations } = declaration
40
+ for (const declaration of declarations) {
41
+ const id = declaration.id
42
+ if (id.type === 'Identifier') {
43
+ exports.add(id.name)
44
+ }
45
+ }
46
+ }
47
+ if (declaration?.type === 'FunctionDeclaration') {
48
+ exports.add(declaration.id.name)
49
+ }
50
+ if (declaration?.type === 'ClassDeclaration') {
51
+ exports.add(declaration.id.name)
52
+ }
53
+ } else if (node.type === 'ExportDefaultDeclaration') {
54
+ exports.add('default')
55
+ }
56
+ }
57
+
58
+ return Array.from(exports).sort()
59
+ }
@@ -0,0 +1,13 @@
1
+ import assert from 'node:assert'
2
+
3
+ export function invariant(
4
+ condition: unknown,
5
+ message = 'Unexpected invariant violation',
6
+ ): asserts condition {
7
+ if (!condition) {
8
+ assert(
9
+ condition,
10
+ `[astrobook] Unexpected internal error. Please open an issue at https://github.com/ocavue/astrobook/issues. ${message}`,
11
+ )
12
+ }
13
+ }
@@ -0,0 +1,30 @@
1
+ // just-kebab-case@4.2.0, inlined.
2
+ //
3
+ // Upstream's get-story-modules.ts imports `just-kebab-case`, but @astrobook/core declares it as a
4
+ // DEV dependency only: tsdown bundles the function into the published dist, so the package never
5
+ // installs it. This package ships source with no bundler, so the function has to come from
6
+ // somewhere — and it decides every story id, which is what lab URLs and the cull/responsive
7
+ // mark files are keyed on. The three regexes below were extracted from the bundle that shipped in
8
+ // @astrobook/core 0.13.3 (dist/index.js, its `just-kebab-case@4.2.0` region) so the ids cannot
9
+ // drift.
10
+ //
11
+ // MIT, Copyright (c) 2016 Angus Croll — https://github.com/angus-c/just
12
+
13
+ const wordSeparators = /[\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]+/
14
+ const capital_plus_lower = /[A-ZÀ-Ý\u00C0-\u00D6\u00D9-\u00DD][a-zà-ÿ]/g
15
+ const capitals = /[A-ZÀ-Ý\u00C0-\u00D6\u00D9-\u00DD]+/g
16
+
17
+ export default function kebabCase(str: string): string {
18
+ str = str.replace(capital_plus_lower, function (match) {
19
+ return ' ' + (match[0].toLowerCase() || match[0]) + match[1]
20
+ })
21
+ str = str.replace(capitals, function (match) {
22
+ return ' ' + match.toLowerCase()
23
+ })
24
+ return str
25
+ .trim()
26
+ .split(wordSeparators)
27
+ .join('-')
28
+ .replace(/^-/, '')
29
+ .replace(/-\s*$/, '')
30
+ }
@@ -0,0 +1,45 @@
1
+ // Vendored from astrobook 0.13.3 (utils/path-builder.ts) — see ../LICENSE-astrobook.
2
+ // Divergence: relative imports carry the .ts extension so node can load this graph directly
3
+ // (upstream is bundled before it ships; this package is not).
4
+ import type { AstroConfig } from 'astro'
5
+
6
+ import {
7
+ ensureLeadingSlash,
8
+ ensureTrailingSlash,
9
+ stripLeadingAndTrailingSlashes,
10
+ stripTrailingSlash,
11
+ } from './path.ts'
12
+
13
+ interface PathBuilderOptions {
14
+ trailingSlash?: AstroConfig['trailingSlash']
15
+ }
16
+
17
+ /**
18
+ * @internal
19
+ */
20
+ export type PathBuilder = (...parts: string[]) => string
21
+
22
+ /**
23
+ * Joins multiple path segments into a single URL path with a leading slash,
24
+ * and adds or removes a trailing slash based on config.
25
+ *
26
+ * @internal
27
+ */
28
+ export function createPathBuilder({
29
+ trailingSlash = 'ignore',
30
+ }: PathBuilderOptions): PathBuilder {
31
+ return function buildPath(...parts): string {
32
+ let result = parts
33
+ .map(stripLeadingAndTrailingSlashes)
34
+ .filter(Boolean)
35
+ .join('/')
36
+
37
+ if (trailingSlash === 'always') {
38
+ result = ensureTrailingSlash(result)
39
+ } else if (trailingSlash === 'never') {
40
+ result = stripTrailingSlash(result)
41
+ }
42
+
43
+ return ensureLeadingSlash(result)
44
+ }
45
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Ensure the passed path starts with a leading slash.
3
+ */
4
+ export function ensureLeadingSlash(href: string): string {
5
+ if (href[0] !== '/') href = '/' + href
6
+ return href
7
+ }
8
+
9
+ /**
10
+ * Ensure the passed path ends with a trailing slash.
11
+ */
12
+ export function ensureTrailingSlash(href: string): string {
13
+ if (href[href.length - 1] !== '/') href += '/'
14
+ return href
15
+ }
16
+
17
+ /**
18
+ * Ensure the passed path starts and ends with slashes.
19
+ */
20
+ export function ensureLeadingAndTrailingSlashes(href: string): string {
21
+ href = ensureLeadingSlash(href)
22
+ href = ensureTrailingSlash(href)
23
+ return href
24
+ }
25
+
26
+ /**
27
+ * Ensure the passed path does not start with a leading slash.
28
+ */
29
+ export function stripLeadingSlash(href: string) {
30
+ while (href[0] === '/') href = href.slice(1)
31
+ return href
32
+ }
33
+
34
+ /**
35
+ * Ensure the passed path does not end with a trailing slash.
36
+ */
37
+ export function stripTrailingSlash(href: string) {
38
+ while (href[href.length - 1] === '/') href = href.slice(0, -1)
39
+ return href
40
+ }
41
+
42
+ /**
43
+ * Ensure the passed path does not start and end with slashes.
44
+ */
45
+ export function stripLeadingAndTrailingSlashes(href: string): string {
46
+ href = stripLeadingSlash(href)
47
+ href = stripTrailingSlash(href)
48
+ return href
49
+ }
50
+
51
+ /**
52
+ * Remove the extension from a path.
53
+ */
54
+ export function stripHtmlExtension(path: string) {
55
+ const pathWithoutTrailingSlash = stripTrailingSlash(path)
56
+ return pathWithoutTrailingSlash.endsWith('.html')
57
+ ? pathWithoutTrailingSlash.slice(0, -5)
58
+ : path
59
+ }
60
+
61
+ /**
62
+ * Add '.html' extension to a path.
63
+ */
64
+ export function ensureHtmlExtension(path: string) {
65
+ path = stripLeadingAndTrailingSlashes(path)
66
+ if (!path.endsWith('.html')) {
67
+ path = path ? path + '.html' : '/index.html'
68
+ }
69
+ return ensureLeadingSlash(path)
70
+ }
71
+
72
+ /**
73
+ * Remove the extension from a path.
74
+ */
75
+ export function stripExtension(path: string) {
76
+ const periodIndex = path.lastIndexOf('.')
77
+ return path.slice(0, periodIndex > -1 ? periodIndex : undefined)
78
+ }
79
+
80
+ export { stripLeadingAndTrailingSlashes as stripSlashes }
@@ -0,0 +1,110 @@
1
+ // Vendored from astrobook 0.13.3 (virtual-module/get-story-modules.ts) — see ../LICENSE-astrobook.
2
+ // Divergence: relative imports carry the .ts extension so node can load this graph directly
3
+ // (upstream is bundled before it ships; this package is not).
4
+ // Divergence: just-kebab-case is inlined at ../utils/kebab-case.ts (upstream bundles it).
5
+ import fs from 'node:fs/promises'
6
+ import path from 'node:path'
7
+
8
+ import type { StoryModule } from '../../types/types.ts'
9
+ import { fdir } from 'fdir'
10
+ import slash from 'slash'
11
+
12
+ import { getExports } from '../utils/get-exports.ts'
13
+ import { invariant } from '../utils/invariant.ts'
14
+ import kebabCase from '../utils/kebab-case.ts'
15
+
16
+ /**
17
+ * List the absolute paths of all story files in the given directory.
18
+ */
19
+ async function listStoryFiles(rootDir: string): Promise<string[]> {
20
+ invariant(
21
+ path.isAbsolute(rootDir),
22
+ `Root directory must be absolute, but got '${rootDir}'`,
23
+ )
24
+
25
+ const filePaths = await new fdir()
26
+ .withSymlinks()
27
+ .withFullPaths()
28
+ .normalize()
29
+ .glob('./**/*.stories.{ts,tsx,js,jsx,mts,mtsx,mjs,mjsx}')
30
+ .exclude((dirname) => dirname === 'node_modules')
31
+ .crawl(rootDir)
32
+ .withPromise()
33
+
34
+ return filePaths.sort()
35
+ }
36
+
37
+ type ParsedStoryFile = {
38
+ /**
39
+ * The absolute path to the file. Unix-style slashes are used.
40
+ */
41
+ filePath: string
42
+
43
+ /**
44
+ * Whether the file has a default export
45
+ */
46
+ defaultExport: boolean
47
+
48
+ /**
49
+ * The named exports in the file
50
+ */
51
+ namedExports: string[]
52
+ }
53
+
54
+ async function parseStoryFiles(filePath: string): Promise<ParsedStoryFile> {
55
+ const code = await fs.readFile(filePath, 'utf-8')
56
+ const exports = getExports(code)
57
+ const defaultExport = exports.includes('default')
58
+ const namedExports = exports.filter((name) => name !== 'default')
59
+
60
+ return { filePath, defaultExport, namedExports }
61
+ }
62
+
63
+ export function convertStoryFileToModule(
64
+ rootDir: string,
65
+ file: ParsedStoryFile,
66
+ ): StoryModule | undefined {
67
+ if (file.namedExports.length === 0) {
68
+ console.warn(`[astrobook] File ${file.filePath} has no named exports`)
69
+ return
70
+ }
71
+
72
+ if (!file.defaultExport) {
73
+ console.warn(`[astrobook] File ${file.filePath} has no default export`)
74
+ return
75
+ }
76
+
77
+ const relativePath = path.normalize(path.relative(rootDir, file.filePath))
78
+ const relativePathWithoutStories = relativePath.replace(
79
+ /\.stories\.\w+$/i,
80
+ '',
81
+ )
82
+ const parts = relativePathWithoutStories.split(path.sep)
83
+
84
+ const directory = parts.slice(0, -1).join('/')
85
+ const name = parts.pop()
86
+ invariant(name, `Unexpected file path: ${file.filePath}`)
87
+
88
+ const moduleId = directory
89
+ ? `${directory}/${kebabCase(name)}`
90
+ : kebabCase(name)
91
+
92
+ return {
93
+ id: moduleId,
94
+ name,
95
+ directory,
96
+ importPath: slash(file.filePath),
97
+ stories: file.namedExports.map((name) => {
98
+ return { id: `${moduleId}/${kebabCase(name)}`, name }
99
+ }),
100
+ }
101
+ }
102
+
103
+ export async function getStoryModules(rootDir: string): Promise<StoryModule[]> {
104
+ const storyFilePaths = await listStoryFiles(rootDir)
105
+ const storyFiles = await Promise.all(storyFilePaths.map(parseStoryFiles))
106
+
107
+ return storyFiles
108
+ .map((storyFile) => convertStoryFileToModule(rootDir, storyFile))
109
+ .filter((module) => !!module)
110
+ }
@@ -0,0 +1,9 @@
1
+ // Vendored from astrobook 0.13.3 (virtual-module/story-modules.ts) — see ../LICENSE-astrobook.
2
+ // Divergence: relative imports carry the .ts extension so node can load this graph directly
3
+ // (upstream is bundled before it ships; this package is not).
4
+ import { getStoryModules } from './get-story-modules.ts'
5
+
6
+ export async function loadStoryModules(rootDir: string): Promise<string> {
7
+ const modules = await getStoryModules(rootDir)
8
+ return `export default ${JSON.stringify(modules)}`
9
+ }
@@ -0,0 +1,17 @@
1
+ export const STORY_MODULES_ID = 'virtual:astrobook/story-modules.mjs'
2
+ export const STORY_MODULES_RESOLVED_ID =
3
+ '__virtual_astrobook_story_modules__.mjs'
4
+
5
+ export const GLOBAL_CONFIG_ID = 'virtual:astrobook/global-config.mjs'
6
+ export const GLOBAL_CONFIG_RESOLVED_ID = '__virtual_astrobook_user_config__.mjs'
7
+
8
+ export const COMPONENT_HEAD_ID = 'virtual:astrobook/components/head.mjs'
9
+ export const COMPONENT_HEAD_RESOLVED_ID =
10
+ '__virtual_astrobook_COMPONENT_HEAD__.mjs'
11
+
12
+ export const COMPONENT_HOME_ID = 'virtual:astrobook/components/home.mjs'
13
+ export const COMPONENT_HOME_RESOLVED_ID =
14
+ '__virtual_astrobook_COMPONENT_HOME__.mjs'
15
+
16
+ export const USER_CSS_ID = 'virtual:astrobook/user-css.mjs'
17
+ export const USER_CSS_RESOLVED_ID = '__virtual_astrobook_user_css__.mjs'