@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.
- package/LICENSE +37 -0
- package/README.md +410 -0
- package/bin/lab-cull.mjs +401 -0
- package/bin/pin-gallery.mjs +121 -0
- package/defaults.mjs +120 -0
- package/dist/core/astro-integration.js +130 -0
- package/dist/core/index.js +6 -0
- package/dist/core/lib-paths.js +29 -0
- package/dist/core/options.js +173 -0
- package/dist/core/utils/get-exports.js +52 -0
- package/dist/core/utils/invariant.js +12 -0
- package/dist/core/utils/kebab-case.js +15 -0
- package/dist/core/utils/path-builder.js +22 -0
- package/dist/core/utils/path.js +53 -0
- package/dist/core/virtual-module/get-story-modules.js +60 -0
- package/dist/core/virtual-module/story-modules.js +8 -0
- package/dist/core/virtual-module/virtual-module-ids.js +22 -0
- package/dist/core/virtual-module/virtual-routes.js +83 -0
- package/dist/core/virtual-module/vite-plugin.js +98 -0
- package/docs/PIN-CONTRACT.md +263 -0
- package/docs/PIN.md +429 -0
- package/index.d.ts +279 -0
- package/index.mjs +347 -0
- package/package.json +93 -0
- package/src/Empty.astro +4 -0
- package/src/Home.astro +298 -0
- package/src/LabHead.astro +1102 -0
- package/src/core/LICENSE-astrobook +166 -0
- package/src/core/astro-integration.ts +166 -0
- package/src/core/client.ts +89 -0
- package/src/core/index.ts +7 -0
- package/src/core/lib/components/empty.astro +1 -0
- package/src/core/lib/components/head.astro +1 -0
- package/src/core/lib/components/home.astro +8 -0
- package/src/core/lib/components/with-decorators.astro +22 -0
- package/src/core/lib/pages/app.astro +19 -0
- package/src/core/lib/pages/preview.astro +17 -0
- package/src/core/lib/pages/story.astro +16 -0
- package/src/core/lib-paths.ts +72 -0
- package/src/core/options.ts +262 -0
- package/src/core/utils/get-exports.ts +59 -0
- package/src/core/utils/invariant.ts +13 -0
- package/src/core/utils/kebab-case.ts +30 -0
- package/src/core/utils/path-builder.ts +45 -0
- package/src/core/utils/path.ts +80 -0
- package/src/core/virtual-module/get-story-modules.ts +110 -0
- package/src/core/virtual-module/story-modules.ts +9 -0
- package/src/core/virtual-module/virtual-module-ids.ts +17 -0
- package/src/core/virtual-module/virtual-routes.ts +130 -0
- package/src/core/virtual-module/vite-plugin.ts +125 -0
- package/src/pin/board.mjs +1521 -0
- package/src/pin/index.mjs +666 -0
- package/src/pin/shot.mjs +427 -0
- package/src/pin/source-stamp.mjs +159 -0
- package/src/pin/tickets.mjs +697 -0
- package/src/pin/toolbar.js +3181 -0
- package/src/shell/Browse.astro +371 -0
- package/src/shell/CardGrid.astro +297 -0
- package/src/shell/Viewport.astro +1330 -0
- package/src/shell/index.json.ts +12 -0
- package/src/shell/lab-index.ts +344 -0
- package/src/shell/lab-params.ts +245 -0
- package/src/shell/live-files.mjs +164 -0
- package/src/shell/marks.mjs +136 -0
- package/src/types/index.ts +6 -0
- package/src/types/types.ts +239 -0
- package/src/types/virtual.d.ts +29 -0
- package/src/ui/components/app.astro +13 -0
- package/src/ui/components/build-path.ts +13 -0
- package/src/ui/components/build-tree.ts +108 -0
- package/src/ui/components/collapse-duration.ts +28 -0
- package/src/ui/components/compress-terms.ts +10 -0
- package/src/ui/components/dashboard-layout.astro +39 -0
- package/src/ui/components/home.astro +65 -0
- package/src/ui/components/layout.astro +110 -0
- package/src/ui/components/preview-layout.astro +109 -0
- package/src/ui/components/sidebar-button-fullscreen.astro +38 -0
- package/src/ui/components/sidebar-button-search.astro +23 -0
- package/src/ui/components/sidebar-button-theme.astro +9 -0
- package/src/ui/components/sidebar-button.astro +24 -0
- package/src/ui/components/sidebar-resize-handle.astro +74 -0
- package/src/ui/components/sidebar-search-panel.astro +41 -0
- package/src/ui/components/sidebar-search-script.ts +103 -0
- package/src/ui/components/sidebar-title.astro +17 -0
- package/src/ui/components/sidebar-tree-node.astro +143 -0
- package/src/ui/components/sidebar-tree.astro +84 -0
- package/src/ui/components/sidebar.astro +29 -0
- package/src/ui/components/theme-message.ts +26 -0
- package/src/ui/components/theme-script.astro +71 -0
- package/src/ui/components/theme-toggle.astro +63 -0
- package/src/ui/components/theme.ts +32 -0
- package/src/ui/index.ts +4 -0
- package/src/ui/lab.css +549 -0
- 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'
|