orga-build 0.9.0 → 0.10.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/README.org +282 -33
- package/cli.js +4 -4
- package/lib/__tests__/build.test.js +441 -22
- package/lib/__tests__/dev.test.d.ts +2 -0
- package/lib/__tests__/dev.test.d.ts.map +1 -0
- package/lib/__tests__/dev.test.js +295 -0
- package/lib/__tests__/fixtures.d.ts +8 -0
- package/lib/__tests__/fixtures.d.ts.map +1 -0
- package/lib/__tests__/fixtures.js +61 -0
- package/lib/app.jsx +23 -50
- package/lib/build.d.ts +4 -1
- package/lib/build.d.ts.map +1 -1
- package/lib/build.js +10 -175
- package/lib/config.d.ts +5 -3
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +8 -16
- package/lib/content.d.ts +6 -0
- package/lib/dev-ssr.d.ts +14 -0
- package/lib/dev-ssr.d.ts.map +1 -0
- package/lib/dev-ssr.js +137 -0
- package/lib/endpoint.d.ts +5 -0
- package/lib/endpoint.d.ts.map +1 -1
- package/lib/endpoint.js +1 -0
- package/lib/files.d.ts +8 -3
- package/lib/files.d.ts.map +1 -1
- package/lib/files.js +104 -20
- package/lib/fs.d.ts +0 -5
- package/lib/fs.d.ts.map +1 -1
- package/lib/fs.js +0 -18
- package/lib/html.d.ts +37 -0
- package/lib/html.d.ts.map +1 -0
- package/lib/html.js +101 -0
- package/lib/index.html +0 -1
- package/lib/island-client.d.ts +5 -0
- package/lib/island-client.d.ts.map +1 -0
- package/lib/island-client.js +41 -0
- package/lib/island.d.ts +18 -0
- package/lib/island.d.ts.map +1 -0
- package/lib/island.js +146 -0
- package/lib/island.jsx +108 -0
- package/lib/orga.d.ts +1 -1
- package/lib/orga.d.ts.map +1 -1
- package/lib/orga.js +54 -16
- package/lib/plugin.d.ts +20 -32
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +128 -178
- package/lib/prerender.d.ts +11 -0
- package/lib/prerender.d.ts.map +1 -0
- package/lib/prerender.js +184 -0
- package/lib/serve.d.ts.map +1 -1
- package/lib/serve.js +5 -23
- package/lib/ssr.jsx +9 -14
- package/lib/util.d.ts +0 -22
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +0 -62
- package/lib/vite.d.ts +5 -6
- package/lib/vite.d.ts.map +1 -1
- package/lib/vite.js +74 -23
- package/package.json +5 -8
- package/lib/components.d.ts +0 -2
- package/lib/components.d.ts.map +0 -1
- package/lib/components.js +0 -1
- package/lib/csr.jsx +0 -11
- package/lib/watch.d.ts +0 -10
- package/lib/watch.d.ts.map +0 -1
- package/lib/watch.js +0 -53
package/lib/config.d.ts
CHANGED
|
@@ -9,15 +9,13 @@ export function loadConfig(...files: string[]): Promise<{
|
|
|
9
9
|
export type Config = {
|
|
10
10
|
outDir: string;
|
|
11
11
|
root: string;
|
|
12
|
-
preBuild: string[];
|
|
13
|
-
postBuild: string[];
|
|
14
12
|
/**
|
|
15
13
|
* - Array of Vite plugins
|
|
16
14
|
*/
|
|
17
15
|
vitePlugins: import("vite").PluginOption[];
|
|
18
16
|
containerClass: string[] | string;
|
|
19
17
|
/**
|
|
20
|
-
* - Global stylesheet URLs
|
|
18
|
+
* - Global stylesheet URLs linked from the HTML shell
|
|
21
19
|
*/
|
|
22
20
|
styles?: string[];
|
|
23
21
|
/**
|
|
@@ -28,5 +26,9 @@ export type Config = {
|
|
|
28
26
|
* - Glob patterns for files to exclude from content scanning
|
|
29
27
|
*/
|
|
30
28
|
exclude?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* - Absolute URL the site is served from, e.g. `https://example.com`
|
|
31
|
+
*/
|
|
32
|
+
site?: string | undefined;
|
|
31
33
|
};
|
|
32
34
|
//# sourceMappingURL=config.d.ts.map
|
package/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.js"],"names":[],"mappings":"AA0BA;;;GAGG;AACH,qCAHW,MAAM,EAAE,GACN,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CA4C5D;;YAnEa,MAAM;UACN,MAAM;;;;iBACN,OAAO,MAAM,EAAE,YAAY,EAAE;oBAC7B,MAAM,EAAE,GAAC,MAAM;;;;aACf,MAAM,EAAE;;;;oBACR,OAAO,SAAS,EAAE,aAAa;;;;cAC/B,MAAM,EAAE;;;;WACR,MAAM,GAAG,SAAS"}
|
package/lib/config.js
CHANGED
|
@@ -5,21 +5,18 @@ import path from 'node:path'
|
|
|
5
5
|
* @typedef {Object} Config
|
|
6
6
|
* @property {string} outDir
|
|
7
7
|
* @property {string} root
|
|
8
|
-
* @property {string[]} preBuild
|
|
9
|
-
* @property {string[]} postBuild
|
|
10
8
|
* @property {import('vite').PluginOption[]} vitePlugins - Array of Vite plugins
|
|
11
9
|
* @property {string[]|string} containerClass
|
|
12
|
-
* @property {string[]} [styles] - Global stylesheet URLs
|
|
10
|
+
* @property {string[]} [styles] - Global stylesheet URLs linked from the HTML shell
|
|
13
11
|
* @property {import('unified').PluggableList} [rehypePlugins] - Extra rehype plugins appended to orga-build defaults
|
|
14
12
|
* @property {string[]} [exclude] - Glob patterns for files to exclude from content scanning
|
|
13
|
+
* @property {string | undefined} [site] - Absolute URL the site is served from, e.g. `https://example.com`
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
16
|
/** @type {Config} */
|
|
18
17
|
const defaultConfig = {
|
|
19
18
|
outDir: '.out',
|
|
20
19
|
root: '.',
|
|
21
|
-
preBuild: [],
|
|
22
|
-
postBuild: [],
|
|
23
20
|
vitePlugins: [],
|
|
24
21
|
containerClass: [],
|
|
25
22
|
styles: [],
|
|
@@ -53,17 +50,12 @@ export async function loadConfig(...files) {
|
|
|
53
50
|
continue
|
|
54
51
|
}
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
break
|
|
63
|
-
} catch (err) {
|
|
64
|
-
// Config file exists but has errors
|
|
65
|
-
console.error(`Error loading config from ${file}:`, err)
|
|
66
|
-
}
|
|
53
|
+
// A broken config file fails the command rather than falling back to defaults.
|
|
54
|
+
const module = await import(filePath)
|
|
55
|
+
// Support both default export (recommended) and named exports
|
|
56
|
+
result = { ...defaultConfig, ...(module.default || module) }
|
|
57
|
+
configPath = filePath
|
|
58
|
+
break
|
|
67
59
|
}
|
|
68
60
|
|
|
69
61
|
result.root = resolveConfigPath(result.root)
|
package/lib/content.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ declare module 'orga-build:content' {
|
|
|
8
8
|
data: Record<string, unknown>
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The configured `site` (absolute URL, no trailing slash), or `undefined`.
|
|
13
|
+
* A page's absolute URL is `site + page.slug`.
|
|
14
|
+
*/
|
|
15
|
+
export const site: string | undefined
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
18
|
* Get all content entries matching a path pattern
|
|
13
19
|
* @param path - Optional path prefix to filter by (e.g., 'writing', 'content/writing/2025')
|
package/lib/dev-ssr.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serves pages and endpoints in dev, rendering each request on the server
|
|
3
|
+
* (matching Astro/SvelteKit behaviour):
|
|
4
|
+
* - Endpoint routes are answered from their `GET`/`HEAD` handlers
|
|
5
|
+
* - Pages are SSR-rendered through the `ssr` environment's module runner, so
|
|
6
|
+
* edits are picked up without restarting
|
|
7
|
+
* - Unknown routes fall through to Vite, which answers 404 like the build
|
|
8
|
+
* - Only GET/HEAD requests that accept HTML are rendered; assets pass through
|
|
9
|
+
*
|
|
10
|
+
* @param {string | undefined} site - Absolute site URL, passed to endpoints
|
|
11
|
+
* @returns {import('vite').Plugin}
|
|
12
|
+
*/
|
|
13
|
+
export function devSsrPlugin(site: string | undefined): import("vite").Plugin;
|
|
14
|
+
//# sourceMappingURL=dev-ssr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-ssr.d.ts","sourceRoot":"","sources":["dev-ssr.js"],"names":[],"mappings":"AASA;;;;;;;;;;;GAWG;AACH,mCAHW,MAAM,GAAG,SAAS,GAChB,OAAO,MAAM,EAAE,MAAM,CAqHjC"}
|
package/lib/dev-ssr.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { isCSSRequest, isRunnableDevEnvironment, normalizePath } from 'vite'
|
|
4
|
+
import { resolveEndpointResponse } from './endpoint.js'
|
|
5
|
+
import { readIndexHtml, renderPageHtml } from './html.js'
|
|
6
|
+
import { islandRuntimeId } from './vite.js'
|
|
7
|
+
|
|
8
|
+
const ssrEntry = fileURLToPath(new URL('./ssr.jsx', import.meta.url))
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Serves pages and endpoints in dev, rendering each request on the server
|
|
12
|
+
* (matching Astro/SvelteKit behaviour):
|
|
13
|
+
* - Endpoint routes are answered from their `GET`/`HEAD` handlers
|
|
14
|
+
* - Pages are SSR-rendered through the `ssr` environment's module runner, so
|
|
15
|
+
* edits are picked up without restarting
|
|
16
|
+
* - Unknown routes fall through to Vite, which answers 404 like the build
|
|
17
|
+
* - Only GET/HEAD requests that accept HTML are rendered; assets pass through
|
|
18
|
+
*
|
|
19
|
+
* @param {string | undefined} site - Absolute site URL, passed to endpoints
|
|
20
|
+
* @returns {import('vite').Plugin}
|
|
21
|
+
*/
|
|
22
|
+
export function devSsrPlugin(site) {
|
|
23
|
+
return {
|
|
24
|
+
name: 'orga-build:dev-ssr',
|
|
25
|
+
// Run before other plugins' middlewares (e.g. Cloudflare) so HTML
|
|
26
|
+
// navigation requests are not answered with a 404 first.
|
|
27
|
+
enforce: 'pre',
|
|
28
|
+
|
|
29
|
+
configureServer(server) {
|
|
30
|
+
const ssr = server.environments.ssr
|
|
31
|
+
if (!isRunnableDevEnvironment(ssr)) {
|
|
32
|
+
server.config.logger.warn(
|
|
33
|
+
'[orga-build] the "ssr" environment is not runnable in this process; dev SSR is disabled'
|
|
34
|
+
)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The browser imports islands straight from source, served by Vite
|
|
39
|
+
// under its `base`. The shell's own URLs are rebased by Vite in
|
|
40
|
+
// `transformIndexHtml`; these are injected afterwards.
|
|
41
|
+
const { root, base } = server.config
|
|
42
|
+
/** @param {string} src */
|
|
43
|
+
const islandUrl = (src) =>
|
|
44
|
+
// Outside the root (`..`, or another drive on Windows): Vite's /@fs/ form.
|
|
45
|
+
src.startsWith('..') || path.isAbsolute(src)
|
|
46
|
+
? `${base}@fs/${normalizePath(path.resolve(root, src)).replace(/^\//, '')}`
|
|
47
|
+
: base + src
|
|
48
|
+
const islandScript = base + islandRuntimeId.slice(1)
|
|
49
|
+
// CSS imported by server-rendered code never reaches the browser's
|
|
50
|
+
// module graph: link each file, which Vite serves as plain CSS.
|
|
51
|
+
const styles = () =>
|
|
52
|
+
[...ssr.moduleGraph.idToModuleMap.keys()]
|
|
53
|
+
.filter(
|
|
54
|
+
(id) => isCSSRequest(id) && path.isAbsolute(id) && !id.includes('?')
|
|
55
|
+
)
|
|
56
|
+
.map((id) => islandUrl(normalizePath(path.relative(root, id))))
|
|
57
|
+
|
|
58
|
+
server.middlewares.use(async (req, res, next) => {
|
|
59
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
60
|
+
return next()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// This runs before Vite's own middlewares, so `base` is still in the URL.
|
|
64
|
+
const url = req.url || '/'
|
|
65
|
+
const requestPath = url.split('?')[0]
|
|
66
|
+
if (!requestPath.startsWith(base)) return next()
|
|
67
|
+
// Directory-style URLs (`/docs/`) are the same page as `/docs`.
|
|
68
|
+
const pathname = requestPath
|
|
69
|
+
.slice(base.length - 1)
|
|
70
|
+
.replace(/(.)\/+$/, '$1')
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// The runner follows the module graph, so stale modules are never served.
|
|
74
|
+
const { render, pages, endpoints } = await ssr.runner.import(ssrEntry)
|
|
75
|
+
|
|
76
|
+
// Endpoint routes are handled first and bypass HTML rendering.
|
|
77
|
+
const endpointModule = endpoints?.[pathname]
|
|
78
|
+
if (endpointModule) {
|
|
79
|
+
const ctx = {
|
|
80
|
+
url: new URL(url, `http://${req.headers.host || 'localhost'}`),
|
|
81
|
+
params: {},
|
|
82
|
+
mode: /** @type {'dev'} */ ('dev'),
|
|
83
|
+
route: { route: pathname },
|
|
84
|
+
site
|
|
85
|
+
}
|
|
86
|
+
const response = await resolveEndpointResponse(
|
|
87
|
+
endpointModule,
|
|
88
|
+
ctx,
|
|
89
|
+
req.method
|
|
90
|
+
)
|
|
91
|
+
res.statusCode = response.status
|
|
92
|
+
response.headers.forEach((headerValue, headerName) => {
|
|
93
|
+
res.setHeader(headerName, headerValue)
|
|
94
|
+
})
|
|
95
|
+
if (req.method === 'HEAD') {
|
|
96
|
+
res.end()
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
res.end(Buffer.from(await response.arrayBuffer()))
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Only handle browser-like navigation requests.
|
|
104
|
+
// Don't match generic */* accepts to avoid hijacking API requests.
|
|
105
|
+
const accept = req.headers.accept || ''
|
|
106
|
+
if (!accept.includes('text/html')) {
|
|
107
|
+
return next()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Don't intercept asset requests (files with extensions)
|
|
111
|
+
if (pathname !== '/' && /\.\w+$/.test(pathname)) {
|
|
112
|
+
return next()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!pages[pathname]) return next()
|
|
116
|
+
|
|
117
|
+
const template = await server.transformIndexHtml(
|
|
118
|
+
url,
|
|
119
|
+
await readIndexHtml(server.config.root)
|
|
120
|
+
)
|
|
121
|
+
const html = renderPageHtml(template, {
|
|
122
|
+
content: render(pathname, islandUrl),
|
|
123
|
+
page: pages[pathname],
|
|
124
|
+
islandScript,
|
|
125
|
+
styles: styles()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
res.statusCode = 200
|
|
129
|
+
res.setHeader('Content-Type', 'text/html')
|
|
130
|
+
res.end(html)
|
|
131
|
+
} catch (e) {
|
|
132
|
+
next(e)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @property {Record<string, string>} params
|
|
5
5
|
* @property {'dev' | 'build'} mode
|
|
6
6
|
* @property {{ route: string }} route
|
|
7
|
+
* @property {string | undefined} site - The configured `site`, without a trailing slash
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
9
10
|
* @param {Record<string, any>} endpointModule
|
|
@@ -19,5 +20,9 @@ export type EndpointContext = {
|
|
|
19
20
|
route: {
|
|
20
21
|
route: string;
|
|
21
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* - The configured `site`, without a trailing slash
|
|
25
|
+
*/
|
|
26
|
+
site: string | undefined;
|
|
22
27
|
};
|
|
23
28
|
//# sourceMappingURL=endpoint.d.ts.map
|
package/lib/endpoint.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["endpoint.js"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["endpoint.js"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wDALW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,OACnB,eAAe,WACf,MAAM,GACJ,OAAO,CAAC,QAAQ,CAAC,CAoC7B;;SA/Ca,GAAG;YACH,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;UACtB,KAAK,GAAG,OAAO;WACf;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE;;;;UACjB,MAAM,GAAG,SAAS"}
|
package/lib/endpoint.js
CHANGED
package/lib/files.d.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* @param {object} [options]
|
|
4
4
|
* @param {string} [options.outDir] - Output directory to exclude from file discovery
|
|
5
5
|
* @param {string[]} [options.exclude] - Additional glob patterns to exclude from file discovery
|
|
6
|
+
* @param {boolean} [options.drafts] - Include pages marked `#+draft:` (for previews in dev)
|
|
6
7
|
*/
|
|
7
|
-
export function setup(dir: string, { outDir, exclude }?: {
|
|
8
|
+
export function setup(dir: string, { outDir, exclude, drafts }?: {
|
|
8
9
|
outDir?: string | undefined;
|
|
9
10
|
exclude?: string[] | undefined;
|
|
11
|
+
drafts?: boolean | undefined;
|
|
10
12
|
}): {
|
|
11
13
|
pages: (() => Promise<Record<string, Page>>) & {
|
|
12
14
|
invalidate: () => void;
|
|
@@ -33,11 +35,14 @@ export function setup(dir: string, { outDir, exclude }?: {
|
|
|
33
35
|
*/
|
|
34
36
|
export function getSlugFromContentFilePath(contentFilePath: string): string;
|
|
35
37
|
export type Page = {
|
|
38
|
+
/**
|
|
39
|
+
* Path to the page data file
|
|
40
|
+
*/
|
|
36
41
|
dataPath: string;
|
|
37
42
|
/**
|
|
38
|
-
*
|
|
43
|
+
* Metadata: an org page's keywords, or a TSX/JSX page's literal named exports
|
|
39
44
|
*/
|
|
40
|
-
|
|
45
|
+
data: Record<string, unknown>;
|
|
41
46
|
};
|
|
42
47
|
export type EndpointRoute = {
|
|
43
48
|
route: string;
|
package/lib/files.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.js"],"names":[],"mappings":"AAsFA;;;;;;GAMG;AACH,2BANW,MAAM,gCAEd;IAAyB,MAAM;IACJ,OAAO;IACR,MAAM;CAClC;;0BA8QqD,IAAI;;iBAlH7C,MAAM;;0BAkHmC,IAAI;;sBA5G7C,MAAM;;0BA4GmC,IAAI;;;0BAAJ,IAAI;;;0BAAJ,IAAI;;;EAvGzD;AA4HD;;;GAGG;AACH,4DAFW,MAAM,UAoBhB;;;;;cA7Ya,MAAM;;;;UAEN,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;WAMvB,MAAM;cACN,MAAM;;;QAKN,MAAM;UACN,MAAM;UACN,MAAM;cACN,MAAM;SACN,KAAK,GAAG,KAAK,GAAG,KAAK;UACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
|
package/lib/files.js
CHANGED
|
@@ -2,12 +2,14 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { globby } from 'globby'
|
|
4
4
|
import { getSettings } from 'orga'
|
|
5
|
+
import { parseSync } from 'vite'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {Object} Page
|
|
8
9
|
* @property {string} dataPath
|
|
9
|
-
* @property {string} [title]
|
|
10
10
|
* Path to the page data file
|
|
11
|
+
* @property {Record<string, unknown>} data
|
|
12
|
+
* Metadata: an org page's keywords, or a TSX/JSX page's literal named exports
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -87,8 +89,9 @@ function getContentId(slug) {
|
|
|
87
89
|
* @param {object} [options]
|
|
88
90
|
* @param {string} [options.outDir] - Output directory to exclude from file discovery
|
|
89
91
|
* @param {string[]} [options.exclude] - Additional glob patterns to exclude from file discovery
|
|
92
|
+
* @param {boolean} [options.drafts] - Include pages marked `#+draft:` (for previews in dev)
|
|
90
93
|
*/
|
|
91
|
-
export function setup(dir, { outDir, exclude = [] } = {}) {
|
|
94
|
+
export function setup(dir, { outDir, exclude = [], drafts = false } = {}) {
|
|
92
95
|
const outDirRelative = outDir ? path.relative(dir, outDir) : null
|
|
93
96
|
// Only exclude outDir if it's inside the root (not an external path like ../out)
|
|
94
97
|
const outDirExclude =
|
|
@@ -125,7 +128,10 @@ export function setup(dir, { outDir, exclude = [] } = {}) {
|
|
|
125
128
|
|
|
126
129
|
if (pageSlug) {
|
|
127
130
|
assertUniqueRoute(routeOwners, pageSlug, 'page', absolutePath)
|
|
128
|
-
|
|
131
|
+
const data = await readMetadata(absolutePath)
|
|
132
|
+
if (drafts || !isDraft(data)) {
|
|
133
|
+
pages[pageSlug] = { dataPath: absolutePath, data }
|
|
134
|
+
}
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
if (endpointRoute) {
|
|
@@ -207,29 +213,13 @@ export function setup(dir, { outDir, exclude = [] } = {}) {
|
|
|
207
213
|
// Derive id from the slug (last segment or 'index')
|
|
208
214
|
const id = getContentId(slug)
|
|
209
215
|
|
|
210
|
-
/** @type {Record<string, unknown>} */
|
|
211
|
-
let data = {}
|
|
212
|
-
|
|
213
|
-
// Extract metadata from .org files
|
|
214
|
-
if (ext === 'org') {
|
|
215
|
-
try {
|
|
216
|
-
const content = await readFile(filePath, 'utf-8')
|
|
217
|
-
data = getSettings(content)
|
|
218
|
-
} catch (/** @type {any} */ error) {
|
|
219
|
-
console.warn(
|
|
220
|
-
`Failed to read metadata from ${filePath}:`,
|
|
221
|
-
error?.message || error
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
216
|
entries.push({
|
|
227
217
|
id,
|
|
228
218
|
slug,
|
|
229
219
|
path: derivedPath,
|
|
230
220
|
filePath,
|
|
231
221
|
ext,
|
|
232
|
-
data
|
|
222
|
+
data: pageData.data
|
|
233
223
|
})
|
|
234
224
|
}
|
|
235
225
|
|
|
@@ -269,6 +259,100 @@ export function setup(dir, { outDir, exclude = [] } = {}) {
|
|
|
269
259
|
}
|
|
270
260
|
}
|
|
271
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Read a page's metadata: an org file's keywords, or the named exports of a
|
|
264
|
+
* TSX/JSX page whose values are literals (`export const title = 'Hi'`).
|
|
265
|
+
* @param {string} filePath
|
|
266
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
267
|
+
*/
|
|
268
|
+
async function readMetadata(filePath) {
|
|
269
|
+
try {
|
|
270
|
+
const text = await readFile(filePath, 'utf-8')
|
|
271
|
+
return filePath.endsWith('.org')
|
|
272
|
+
? getSettings(text)
|
|
273
|
+
: readLiteralExports(filePath, text)
|
|
274
|
+
} catch (/** @type {any} */ error) {
|
|
275
|
+
console.warn(
|
|
276
|
+
`Failed to read metadata from ${filePath}:`,
|
|
277
|
+
error?.message || error
|
|
278
|
+
)
|
|
279
|
+
return {}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Named exports whose values can be read without running the module. Others
|
|
285
|
+
* are skipped; Vite reports syntax errors when it builds the page.
|
|
286
|
+
* @param {string} filePath
|
|
287
|
+
* @param {string} text
|
|
288
|
+
*/
|
|
289
|
+
function readLiteralExports(filePath, text) {
|
|
290
|
+
const { program, errors } = parseSync(filePath, text)
|
|
291
|
+
/** @type {Record<string, unknown>} */
|
|
292
|
+
const data = {}
|
|
293
|
+
if (errors.length) return data
|
|
294
|
+
for (const node of program.body) {
|
|
295
|
+
if (
|
|
296
|
+
node.type !== 'ExportNamedDeclaration' ||
|
|
297
|
+
node.declaration?.type !== 'VariableDeclaration'
|
|
298
|
+
) {
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
for (const { id, init } of node.declaration.declarations) {
|
|
302
|
+
if (id.type !== 'Identifier' || !init) continue
|
|
303
|
+
const value = literalValue(init)
|
|
304
|
+
if (value !== notLiteral) data[id.name] = value
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return data
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const notLiteral = Symbol('notLiteral')
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The value of a literal expression: a string, number (signed too), boolean
|
|
314
|
+
* or `null`, a template without placeholders, or an array of those.
|
|
315
|
+
* @param {any} node
|
|
316
|
+
* @returns {unknown}
|
|
317
|
+
*/
|
|
318
|
+
function literalValue(node) {
|
|
319
|
+
switch (node.type) {
|
|
320
|
+
case 'Literal':
|
|
321
|
+
// Regexes and bigints have no JSON form.
|
|
322
|
+
return node.regex || node.bigint ? notLiteral : node.value
|
|
323
|
+
case 'TemplateLiteral':
|
|
324
|
+
return node.expressions.length ? notLiteral : node.quasis[0].value.cooked
|
|
325
|
+
case 'TSAsExpression':
|
|
326
|
+
case 'TSSatisfiesExpression':
|
|
327
|
+
return literalValue(node.expression)
|
|
328
|
+
case 'UnaryExpression': {
|
|
329
|
+
const value = literalValue(node.argument)
|
|
330
|
+
if (typeof value !== 'number') return notLiteral
|
|
331
|
+
return node.operator === '-'
|
|
332
|
+
? -value
|
|
333
|
+
: node.operator === '+'
|
|
334
|
+
? value
|
|
335
|
+
: notLiteral
|
|
336
|
+
}
|
|
337
|
+
case 'ArrayExpression': {
|
|
338
|
+
const values = node.elements.map((/** @type {any} */ element) =>
|
|
339
|
+
element ? literalValue(element) : notLiteral
|
|
340
|
+
)
|
|
341
|
+
return values.includes(notLiteral) ? notLiteral : values
|
|
342
|
+
}
|
|
343
|
+
default:
|
|
344
|
+
return notLiteral
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Whether a page is marked `#+draft: t` (or `true`, `yes`).
|
|
350
|
+
* @param {Record<string, unknown>} data
|
|
351
|
+
*/
|
|
352
|
+
function isDraft(data) {
|
|
353
|
+
return /^(t|true|yes)$/i.test(String(data.draft ?? '').trim())
|
|
354
|
+
}
|
|
355
|
+
|
|
272
356
|
/**
|
|
273
357
|
* Creates a cached version of an async function that will only execute once
|
|
274
358
|
* and return the cached result on subsequent calls. The returned function
|
package/lib/fs.d.ts
CHANGED
|
@@ -9,11 +9,6 @@ export function resolvePath(rootPath: string): string;
|
|
|
9
9
|
* @returns {Promise<boolean>}
|
|
10
10
|
*/
|
|
11
11
|
export function exists(path: string): Promise<boolean>;
|
|
12
|
-
/**
|
|
13
|
-
* @param {string} dir
|
|
14
|
-
* @returns {Promise<void>}
|
|
15
|
-
*/
|
|
16
|
-
export function emptyDir(dir: string): Promise<void>;
|
|
17
12
|
/**
|
|
18
13
|
* @param {string} path
|
|
19
14
|
*/
|
package/lib/fs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["fs.js"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAclB;AAED;;;GAGG;AACH,6BAHW,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAS5B;AAED
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["fs.js"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAclB;AAED;;;GAGG;AACH,6BAHW,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAS5B;AAED;;GAEG;AACH,gCAFW,MAAM,iBAUhB;AAED;;;GAGG;AACH,0BAHW,MAAM,QACN,MAAM,iBAKhB"}
|
package/lib/fs.js
CHANGED
|
@@ -33,24 +33,6 @@ export async function exists(path) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* @param {string} dir
|
|
38
|
-
* @returns {Promise<void>}
|
|
39
|
-
*/
|
|
40
|
-
export async function emptyDir(dir) {
|
|
41
|
-
/** @type {string[]} */
|
|
42
|
-
let items = []
|
|
43
|
-
try {
|
|
44
|
-
items = await fs.readdir(dir)
|
|
45
|
-
} catch {
|
|
46
|
-
await fs.mkdir(dir, { recursive: true })
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
await Promise.all(
|
|
50
|
-
items.map((item) => fs.rm(`${dir}/${item}`, { recursive: true }))
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
36
|
/**
|
|
55
37
|
* @param {string} path
|
|
56
38
|
*/
|
package/lib/html.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the HTML shell: the user's `index.html` in the Vite root, or the
|
|
3
|
+
* default one shipped with orga-build.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} root - Vite root
|
|
6
|
+
*/
|
|
7
|
+
export function readIndexHtml(root: string): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Makes `<root>/index.html` always loadable, falling back to the default shell,
|
|
10
|
+
* so Vite can use it as the client build entry. Vite then bundles its scripts
|
|
11
|
+
* and stylesheets and injects the hashed asset tags itself.
|
|
12
|
+
*
|
|
13
|
+
* Global styles are added as `<link>` tags, which Vite serves (with HMR) in
|
|
14
|
+
* dev and bundles in build.
|
|
15
|
+
*
|
|
16
|
+
* @param {string[]} [styles]
|
|
17
|
+
* @returns {import('vite').Plugin}
|
|
18
|
+
*/
|
|
19
|
+
export function htmlShellPlugin(styles?: string[]): import("vite").Plugin;
|
|
20
|
+
/**
|
|
21
|
+
* Fill a processed HTML shell with a server-rendered page. The island runtime
|
|
22
|
+
* is only linked when the page has an island, so other pages ship no script.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} template - HTML shell, already transformed by Vite
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {string | undefined} options.content - Rendered page markup
|
|
27
|
+
* @param {Record<string, unknown> | undefined} options.page - Page module exports, used for `%orga.*%` placeholders
|
|
28
|
+
* @param {string | undefined} [options.islandScript] - URL of the island runtime
|
|
29
|
+
* @param {string[]} [options.styles] - URLs of stylesheets imported by server-rendered code
|
|
30
|
+
*/
|
|
31
|
+
export function renderPageHtml(template: string, { content, page, islandScript, styles }: {
|
|
32
|
+
content: string | undefined;
|
|
33
|
+
page: Record<string, unknown> | undefined;
|
|
34
|
+
islandScript?: string | undefined;
|
|
35
|
+
styles?: string[] | undefined;
|
|
36
|
+
}): string;
|
|
37
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["html.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,oCAFW,MAAM,mBAMhB;AAED;;;;;;;;;;GAUG;AACH,yCAHW,MAAM,EAAE,GACN,OAAO,MAAM,EAAE,MAAM,CAqCjC;AAED;;;;;;;;;;GAUG;AACH,yCAPW,MAAM,2CAEd;IAAoC,OAAO,EAAnC,MAAM,GAAG,SAAS;IAC2B,IAAI,EAAjD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IACN,YAAY,GAAzC,MAAM,GAAG,SAAS;IACC,MAAM;CACnC,UAsBA"}
|
package/lib/html.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { exists } from './fs.js'
|
|
5
|
+
import { escapeHtml } from './util.js'
|
|
6
|
+
|
|
7
|
+
const defaultIndexHtml = fileURLToPath(new URL('./index.html', import.meta.url))
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read the HTML shell: the user's `index.html` in the Vite root, or the
|
|
11
|
+
* default one shipped with orga-build.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} root - Vite root
|
|
14
|
+
*/
|
|
15
|
+
export async function readIndexHtml(root) {
|
|
16
|
+
const userIndexHtml = path.join(root, 'index.html')
|
|
17
|
+
const file = (await exists(userIndexHtml)) ? userIndexHtml : defaultIndexHtml
|
|
18
|
+
return fs.readFile(file, 'utf-8')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Makes `<root>/index.html` always loadable, falling back to the default shell,
|
|
23
|
+
* so Vite can use it as the client build entry. Vite then bundles its scripts
|
|
24
|
+
* and stylesheets and injects the hashed asset tags itself.
|
|
25
|
+
*
|
|
26
|
+
* Global styles are added as `<link>` tags, which Vite serves (with HMR) in
|
|
27
|
+
* dev and bundles in build.
|
|
28
|
+
*
|
|
29
|
+
* @param {string[]} [styles]
|
|
30
|
+
* @returns {import('vite').Plugin}
|
|
31
|
+
*/
|
|
32
|
+
export function htmlShellPlugin(styles = []) {
|
|
33
|
+
/** @type {string} */
|
|
34
|
+
let root
|
|
35
|
+
/** @type {string} */
|
|
36
|
+
let indexHtmlPath
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: 'orga-build:html-shell',
|
|
40
|
+
enforce: 'pre',
|
|
41
|
+
configResolved(config) {
|
|
42
|
+
root = config.root
|
|
43
|
+
indexHtmlPath = path.join(root, 'index.html')
|
|
44
|
+
},
|
|
45
|
+
resolveId(id, importer) {
|
|
46
|
+
// Build entries arrive relative to the root, without an importer.
|
|
47
|
+
if (!importer && path.resolve(root, id) === indexHtmlPath) {
|
|
48
|
+
return indexHtmlPath
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async load(id) {
|
|
52
|
+
if (id === indexHtmlPath) {
|
|
53
|
+
return readIndexHtml(root)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
transformIndexHtml: {
|
|
57
|
+
order: 'pre',
|
|
58
|
+
handler() {
|
|
59
|
+
return [...new Set(styles)].map((href) => ({
|
|
60
|
+
tag: 'link',
|
|
61
|
+
attrs: { rel: 'stylesheet', href },
|
|
62
|
+
injectTo: 'head'
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fill a processed HTML shell with a server-rendered page. The island runtime
|
|
71
|
+
* is only linked when the page has an island, so other pages ship no script.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} template - HTML shell, already transformed by Vite
|
|
74
|
+
* @param {Object} options
|
|
75
|
+
* @param {string | undefined} options.content - Rendered page markup
|
|
76
|
+
* @param {Record<string, unknown> | undefined} options.page - Page module exports, used for `%orga.*%` placeholders
|
|
77
|
+
* @param {string | undefined} [options.islandScript] - URL of the island runtime
|
|
78
|
+
* @param {string[]} [options.styles] - URLs of stylesheets imported by server-rendered code
|
|
79
|
+
*/
|
|
80
|
+
export function renderPageHtml(
|
|
81
|
+
template,
|
|
82
|
+
{ content, page, islandScript, styles = [] }
|
|
83
|
+
) {
|
|
84
|
+
let html = template
|
|
85
|
+
// Unknown routes render nothing (`undefined`); a page may render ''.
|
|
86
|
+
if (content !== undefined) {
|
|
87
|
+
html = html.replace(
|
|
88
|
+
'<div id="root"></div>',
|
|
89
|
+
`<div id="root">${content}</div>`
|
|
90
|
+
)
|
|
91
|
+
const head = styles.map((href) => `<link rel="stylesheet" href="${href}">`)
|
|
92
|
+
if (islandScript && content.includes('<orga-island')) {
|
|
93
|
+
head.push(`<script type="module" src="${islandScript}"></script>`)
|
|
94
|
+
}
|
|
95
|
+
if (head.length) html = html.replace(/<\/head>/i, `${head.join('')}$&`)
|
|
96
|
+
}
|
|
97
|
+
// Unknown routes have no page: their placeholders resolve to empty strings.
|
|
98
|
+
return html.replace(/%orga\.(\w+)%/g, (_, key) =>
|
|
99
|
+
escapeHtml(String(page?.[key] ?? ''))
|
|
100
|
+
)
|
|
101
|
+
}
|