orga-build 0.8.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.
Files changed (66) hide show
  1. package/README.org +296 -41
  2. package/cli.js +4 -4
  3. package/lib/__tests__/build.test.js +444 -23
  4. package/lib/__tests__/dev.test.d.ts +2 -0
  5. package/lib/__tests__/dev.test.d.ts.map +1 -0
  6. package/lib/__tests__/dev.test.js +295 -0
  7. package/lib/__tests__/fixtures.d.ts +8 -0
  8. package/lib/__tests__/fixtures.d.ts.map +1 -0
  9. package/lib/__tests__/fixtures.js +61 -0
  10. package/lib/app.jsx +23 -50
  11. package/lib/build.d.ts +4 -1
  12. package/lib/build.d.ts.map +1 -1
  13. package/lib/build.js +12 -176
  14. package/lib/config.d.ts +9 -3
  15. package/lib/config.d.ts.map +1 -1
  16. package/lib/config.js +14 -18
  17. package/lib/content.d.ts +6 -0
  18. package/lib/dev-ssr.d.ts +14 -0
  19. package/lib/dev-ssr.d.ts.map +1 -0
  20. package/lib/dev-ssr.js +137 -0
  21. package/lib/endpoint.d.ts +5 -0
  22. package/lib/endpoint.d.ts.map +1 -1
  23. package/lib/endpoint.js +1 -0
  24. package/lib/files.d.ts +10 -3
  25. package/lib/files.d.ts.map +1 -1
  26. package/lib/files.js +111 -23
  27. package/lib/fs.d.ts +0 -5
  28. package/lib/fs.d.ts.map +1 -1
  29. package/lib/fs.js +0 -18
  30. package/lib/html.d.ts +37 -0
  31. package/lib/html.d.ts.map +1 -0
  32. package/lib/html.js +101 -0
  33. package/lib/index.html +0 -1
  34. package/lib/island-client.d.ts +5 -0
  35. package/lib/island-client.d.ts.map +1 -0
  36. package/lib/island-client.js +41 -0
  37. package/lib/island.d.ts +18 -0
  38. package/lib/island.d.ts.map +1 -0
  39. package/lib/island.js +146 -0
  40. package/lib/island.jsx +108 -0
  41. package/lib/orga.d.ts +1 -1
  42. package/lib/orga.d.ts.map +1 -1
  43. package/lib/orga.js +54 -16
  44. package/lib/plugin.d.ts +25 -32
  45. package/lib/plugin.d.ts.map +1 -1
  46. package/lib/plugin.js +130 -170
  47. package/lib/prerender.d.ts +11 -0
  48. package/lib/prerender.d.ts.map +1 -0
  49. package/lib/prerender.js +184 -0
  50. package/lib/serve.d.ts.map +1 -1
  51. package/lib/serve.js +5 -23
  52. package/lib/ssr.jsx +9 -14
  53. package/lib/util.d.ts +0 -22
  54. package/lib/util.d.ts.map +1 -1
  55. package/lib/util.js +0 -62
  56. package/lib/vite.d.ts +7 -6
  57. package/lib/vite.d.ts.map +1 -1
  58. package/lib/vite.js +75 -23
  59. package/package.json +5 -8
  60. package/lib/components.d.ts +0 -2
  61. package/lib/components.d.ts.map +0 -1
  62. package/lib/components.js +0 -1
  63. package/lib/csr.jsx +0 -11
  64. package/lib/watch.d.ts +0 -10
  65. package/lib/watch.d.ts.map +0 -1
  66. package/lib/watch.js +0 -53
package/lib/config.d.ts CHANGED
@@ -9,20 +9,26 @@ 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 injected in dev SSR and imported by client entry
18
+ * - Global stylesheet URLs linked from the HTML shell
21
19
  */
22
20
  styles?: string[];
23
21
  /**
24
22
  * - Extra rehype plugins appended to orga-build defaults
25
23
  */
26
24
  rehypePlugins?: import("unified").PluggableList;
25
+ /**
26
+ * - Glob patterns for files to exclude from content scanning
27
+ */
28
+ exclude?: string[];
29
+ /**
30
+ * - Absolute URL the site is served from, e.g. `https://example.com`
31
+ */
32
+ site?: string | undefined;
27
33
  };
28
34
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.js"],"names":[],"mappings":"AA2BA;;;GAGG;AACH,qCAHW,MAAM,EAAE,GACN,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CA+C5D;;YAvEa,MAAM;UACN,MAAM;cACN,MAAM,EAAE;eACR,MAAM,EAAE;;;;iBACR,OAAO,MAAM,EAAE,YAAY,EAAE;oBAC7B,MAAM,EAAE,GAAC,MAAM;;;;aACf,MAAM,EAAE;;;;oBACR,OAAO,SAAS,EAAE,aAAa"}
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,24 +5,23 @@ 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 injected in dev SSR and imported by client entry
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
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`
14
14
  */
15
15
 
16
16
  /** @type {Config} */
17
17
  const defaultConfig = {
18
18
  outDir: '.out',
19
19
  root: '.',
20
- preBuild: [],
21
- postBuild: [],
22
20
  vitePlugins: [],
23
21
  containerClass: [],
24
22
  styles: [],
25
- rehypePlugins: []
23
+ rehypePlugins: [],
24
+ exclude: []
26
25
  }
27
26
 
28
27
  /**
@@ -51,24 +50,21 @@ export async function loadConfig(...files) {
51
50
  continue
52
51
  }
53
52
 
54
- try {
55
- const module = await import(filePath)
56
- // Support both default export (recommended) and named exports
57
- const config = module.default || module
58
- result = { ...defaultConfig, ...config }
59
- configPath = filePath
60
- break
61
- } catch (err) {
62
- // Config file exists but has errors
63
- console.error(`Error loading config from ${file}:`, err)
64
- }
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
65
59
  }
66
60
 
67
61
  result.root = resolveConfigPath(result.root)
68
62
  result.outDir = resolveConfigPath(result.outDir)
69
63
  const styles = result.styles
70
64
  result.styles = Array.isArray(styles)
71
- ? styles.filter((v) => typeof v === 'string')
65
+ ? styles
66
+ .filter((v) => typeof v === 'string')
67
+ .map((v) => '/' + v.replace(/^\/+/, ''))
72
68
  : []
73
69
  return {
74
70
  config: result,
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')
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["endpoint.js"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;GAKG;AACH,wDALW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,OACnB,eAAe,WACf,MAAM,GACJ,OAAO,CAAC,QAAQ,CAAC,CAoC7B;;SA9Ca,GAAG;YACH,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;UACtB,KAAK,GAAG,OAAO;WACf;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE"}
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
@@ -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
  /**
package/lib/files.d.ts CHANGED
@@ -2,9 +2,13 @@
2
2
  * @param {string} dir
3
3
  * @param {object} [options]
4
4
  * @param {string} [options.outDir] - Output directory to exclude from file discovery
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)
5
7
  */
6
- export function setup(dir: string, { outDir }?: {
8
+ export function setup(dir: string, { outDir, exclude, drafts }?: {
7
9
  outDir?: string | undefined;
10
+ exclude?: string[] | undefined;
11
+ drafts?: boolean | undefined;
8
12
  }): {
9
13
  pages: (() => Promise<Record<string, Page>>) & {
10
14
  invalidate: () => void;
@@ -31,11 +35,14 @@ export function setup(dir: string, { outDir }?: {
31
35
  */
32
36
  export function getSlugFromContentFilePath(contentFilePath: string): string;
33
37
  export type Page = {
38
+ /**
39
+ * Path to the page data file
40
+ */
34
41
  dataPath: string;
35
42
  /**
36
- * Path to the page data file
43
+ * Metadata: an org page's keywords, or a TSX/JSX page's literal named exports
37
44
  */
38
- title?: string;
45
+ data: Record<string, unknown>;
39
46
  };
40
47
  export type EndpointRoute = {
41
48
  route: string;
@@ -1 +1 @@
1
- {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.js"],"names":[],"mappings":"AAoFA;;;;GAIG;AACH,2BAJW,MAAM,eAEd;IAAyB,MAAM;CACjC;;0BA0LqD,IAAI;;iBApB7C,MAAM;;0BAoBmC,IAAI;;sBAd7C,MAAM;;0BAcmC,IAAI;;;0BAAJ,IAAI;;;0BAAJ,IAAI;;;EATzD;AA8BD;;;GAGG;AACH,4DAFW,MAAM,UAoBhB;;cAtTa,MAAM;;;;YACN,MAAM;;;WAMN,MAAM;cACN,MAAM;;;QAKN,MAAM;UACN,MAAM;UACN,MAAM;cACN,MAAM;SACN,KAAK,GAAG,KAAK,GAAG,KAAK;UACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
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
  /**
@@ -86,8 +88,10 @@ function getContentId(slug) {
86
88
  * @param {string} dir
87
89
  * @param {object} [options]
88
90
  * @param {string} [options.outDir] - Output directory to exclude from file discovery
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)
89
93
  */
90
- export function setup(dir, { outDir } = {}) {
94
+ export function setup(dir, { outDir, exclude = [], drafts = false } = {}) {
91
95
  const outDirRelative = outDir ? path.relative(dir, outDir) : null
92
96
  // Only exclude outDir if it's inside the root (not an external path like ../out)
93
97
  const outDirExclude =
@@ -104,7 +108,8 @@ export function setup(dir, { outDir } = {}) {
104
108
  '!**/.*/**',
105
109
  '!**/.*',
106
110
  '!node_modules/**',
107
- ...(outDirExclude ? [outDirExclude] : [])
111
+ ...(outDirExclude ? [outDirExclude] : []),
112
+ ...exclude.map((p) => `!${p}`)
108
113
  ],
109
114
  { cwd: dir }
110
115
  )
@@ -123,7 +128,10 @@ export function setup(dir, { outDir } = {}) {
123
128
 
124
129
  if (pageSlug) {
125
130
  assertUniqueRoute(routeOwners, pageSlug, 'page', absolutePath)
126
- pages[pageSlug] = { dataPath: absolutePath }
131
+ const data = await readMetadata(absolutePath)
132
+ if (drafts || !isDraft(data)) {
133
+ pages[pageSlug] = { dataPath: absolutePath, data }
134
+ }
127
135
  }
128
136
 
129
137
  if (endpointRoute) {
@@ -155,7 +163,8 @@ export function setup(dir, { outDir } = {}) {
155
163
  '!**/.*/**',
156
164
  '!**/.*',
157
165
  '!node_modules/**',
158
- ...(outDirExclude ? [outDirExclude] : [])
166
+ ...(outDirExclude ? [outDirExclude] : []),
167
+ ...exclude.map((p) => `!${p}`)
159
168
  ],
160
169
  {
161
170
  cwd: dir
@@ -179,7 +188,8 @@ export function setup(dir, { outDir } = {}) {
179
188
  '!**/.*/**',
180
189
  '!**/.*',
181
190
  '!node_modules/**',
182
- ...(outDirExclude ? [outDirExclude] : [])
191
+ ...(outDirExclude ? [outDirExclude] : []),
192
+ ...exclude.map((p) => `!${p}`)
183
193
  ],
184
194
  {
185
195
  cwd: dir
@@ -203,29 +213,13 @@ export function setup(dir, { outDir } = {}) {
203
213
  // Derive id from the slug (last segment or 'index')
204
214
  const id = getContentId(slug)
205
215
 
206
- /** @type {Record<string, unknown>} */
207
- let data = {}
208
-
209
- // Extract metadata from .org files
210
- if (ext === 'org') {
211
- try {
212
- const content = await readFile(filePath, 'utf-8')
213
- data = getSettings(content)
214
- } catch (/** @type {any} */ error) {
215
- console.warn(
216
- `Failed to read metadata from ${filePath}:`,
217
- error?.message || error
218
- )
219
- }
220
- }
221
-
222
216
  entries.push({
223
217
  id,
224
218
  slug,
225
219
  path: derivedPath,
226
220
  filePath,
227
221
  ext,
228
- data
222
+ data: pageData.data
229
223
  })
230
224
  }
231
225
 
@@ -265,6 +259,100 @@ export function setup(dir, { outDir } = {}) {
265
259
  }
266
260
  }
267
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
+
268
356
  /**
269
357
  * Creates a cached version of an async function that will only execute once
270
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;;;GAGG;AACH,8BAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAczB;AAED;;GAEG;AACH,gCAFW,MAAM,iBAUhB;AAED;;;GAGG;AACH,0BAHW,MAAM,QACN,MAAM,iBAKhB"}
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"}