|
1 | 1 | #!/usr/bin/env bun |
2 | 2 |
|
3 | | -// Parse the browser's inline TypeScript before publishing. The deployed page |
4 | | -// relies on Babel standalone at runtime, so a missing comma or other syntax |
5 | | -// error would otherwise leave the catalog permanently on its loading state. |
| 3 | +// Parse the browser's TypeScript before publishing. The deployed page relies |
| 4 | +// on Babel standalone at runtime, so a missing comma or other syntax error |
| 5 | +// would otherwise leave the catalog permanently on its loading state. |
6 | 6 |
|
7 | 7 | declare const Bun: { |
8 | 8 | file(path: string): { text(): Promise<string> }; |
9 | 9 | Transpiler: new (options: { loader: string }) => { transform(source: string): string | Promise<string> }; |
10 | 10 | }; |
11 | 11 |
|
12 | | -type Match = RegExpExecArray & { 1: string }; |
| 12 | +type BrowserScript = { inline: string } | { src: string }; |
13 | 13 |
|
| 14 | +function attributeValue(attributes: string, name: string): string | null { |
| 15 | + const match = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i').exec(attributes); |
| 16 | + return match ? (match[1] ?? match[2] ?? match[3] ?? '') : null; |
| 17 | +} |
| 18 | + |
| 19 | +export function extractBrowserScript(html: string): BrowserScript { |
| 20 | + const scripts = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi; |
| 21 | + let match: RegExpExecArray | null; |
| 22 | + while ((match = scripts.exec(html))) { |
| 23 | + const type = attributeValue(match[1], 'type'); |
| 24 | + if (type?.toLowerCase() !== 'text/babel') continue; |
| 25 | + const src = attributeValue(match[1], 'src'); |
| 26 | + if (src) return { src }; |
| 27 | + if (match[2].trim()) return { inline: match[2] }; |
| 28 | + } |
| 29 | + throw new Error('index.html: text/babel browser script not found'); |
| 30 | +} |
| 31 | + |
| 32 | +// Keep the original helper for callers that specifically want an inline |
| 33 | +// script, while allowing the page to move its Babel entrypoint into a local |
| 34 | +// .tsx file without making this validation stale. |
14 | 35 | export function extractInlineBrowserScript(html: string): string { |
15 | | - const match = /<script\s+type=["']text\/babel["'][^>]*>([\s\S]*?)<\/script>/i.exec(html) as Match | null; |
16 | | - if (!match || !match[1].trim()) throw new Error('index.html: inline text/babel script not found'); |
17 | | - return match[1]; |
| 36 | + const script = extractBrowserScript(html); |
| 37 | + if ('inline' in script) return script.inline; |
| 38 | + throw new Error(`index.html: text/babel script is external (${script.src})`); |
18 | 39 | } |
19 | 40 |
|
20 | | -export async function parseInlineBrowserScript(source: string): Promise<string> { |
| 41 | +export async function parseBrowserScript(source: string): Promise<string> { |
21 | 42 | const transformed = await new Bun.Transpiler({ loader: 'tsx' }).transform(source); |
22 | | - if (!transformed || typeof transformed !== 'string') throw new Error('index.html: inline script did not transpile'); |
| 43 | + if (!transformed || typeof transformed !== 'string') throw new Error('browser script did not transpile'); |
23 | 44 | return transformed; |
24 | 45 | } |
25 | 46 |
|
| 47 | +// Backwards-compatible name for the inline-script parser used by the earlier |
| 48 | +// single-file layout. |
| 49 | +export const parseInlineBrowserScript = parseBrowserScript; |
| 50 | + |
26 | 51 | if ((import.meta as { main?: boolean }).main) { |
27 | | - const html = await Bun.file(new URL('../index.html', import.meta.url).pathname).text(); |
28 | | - const source = extractInlineBrowserScript(html); |
29 | | - const transformed = await parseInlineBrowserScript(source); |
30 | | - console.log(`index.html inline script parses (${source.length.toLocaleString()} source chars, ${transformed.length.toLocaleString()} transpiled chars)`); |
| 52 | + const indexUrl = new URL('../index.html', import.meta.url); |
| 53 | + const html = await Bun.file(indexUrl.pathname).text(); |
| 54 | + const script = extractBrowserScript(html); |
| 55 | + const sourcePath = 'src' in script ? new URL(script.src, indexUrl) : indexUrl; |
| 56 | + if (sourcePath.protocol !== 'file:') throw new Error(`browser script must be local: ${sourcePath.href}`); |
| 57 | + const source = 'inline' in script ? script.inline : await Bun.file(sourcePath.pathname).text(); |
| 58 | + const transformed = await parseBrowserScript(source); |
| 59 | + const label = 'src' in script ? script.src : 'index.html inline script'; |
| 60 | + console.log(`${label} parses (${source.length.toLocaleString()} source chars, ${transformed.length.toLocaleString()} transpiled chars)`); |
31 | 61 | } |
0 commit comments