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.
Files changed (66) hide show
  1. package/README.org +282 -33
  2. package/cli.js +4 -4
  3. package/lib/__tests__/build.test.js +441 -22
  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 +10 -175
  14. package/lib/config.d.ts +5 -3
  15. package/lib/config.d.ts.map +1 -1
  16. package/lib/config.js +8 -16
  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 +8 -3
  25. package/lib/files.d.ts.map +1 -1
  26. package/lib/files.js +104 -20
  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 +20 -32
  45. package/lib/plugin.d.ts.map +1 -1
  46. package/lib/plugin.js +128 -178
  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 +5 -6
  57. package/lib/vite.d.ts.map +1 -1
  58. package/lib/vite.js +74 -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/util.d.ts CHANGED
@@ -1,28 +1,6 @@
1
- export function buildNav(): void;
2
- /**
3
- * @param {string} file
4
- * @param {...RegExp|string} patterns
5
- * @returns {boolean}
6
- */
7
- export function match(file: string, ...patterns: (RegExp | string)[]): boolean;
8
- /**
9
- * Default layout
10
- * @param {Object} props
11
- * @param {string|undefined} props.title
12
- * @param {import('react').ReactNode} props.children
13
- * @returns {React.JSX.Element}
14
- */
15
- export function DefaultLayout({ title, children }: {
16
- title: string | undefined;
17
- children: import("react").ReactNode;
18
- }): React.JSX.Element;
19
1
  /**
20
2
  * @param {string} str
21
3
  * @returns {string}
22
4
  */
23
5
  export function escapeHtml(str: string): string;
24
- /**
25
- * @param {string} cmd
26
- */
27
- export function $(cmd: string): Promise<any>;
28
6
  //# sourceMappingURL=util.d.ts.map
package/lib/util.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["util.js"],"names":[],"mappings":"AAGA,iCAA6B;AAE7B;;;;GAIG;AACH,4BAJW,MAAM,eACN,CAAG,MAAM,GAAC,MAAM,GAAA,GACd,OAAO,CASnB;AAED;;;;;;GAMG;AACH,mDAJG;IAAgC,KAAK,EAA7B,MAAM,GAAC,SAAS;IACiB,QAAQ,EAAzC,OAAO,OAAO,EAAE,SAAS;CACjC,GAAU,KAAK,CAAC,GAAG,CAAC,OAAO,CAkB7B;AAED;;;GAGG;AACH,gCAHW,MAAM,GACJ,MAAM,CAQlB;AAED;;GAEG;AACH,uBAFW,MAAM,gBAehB"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["util.js"],"names":[],"mappings":"AAAA;;;GAGG;AACH,gCAHW,MAAM,GACJ,MAAM,CAQlB"}
package/lib/util.js CHANGED
@@ -1,47 +1,3 @@
1
- import { exec } from 'node:child_process'
2
- import { createElement } from 'react'
3
-
4
- export function buildNav() {}
5
-
6
- /**
7
- * @param {string} file
8
- * @param {...RegExp|string} patterns
9
- * @returns {boolean}
10
- */
11
- export function match(file, ...patterns) {
12
- return patterns.some((p) => {
13
- if (p instanceof RegExp) {
14
- return p.test(file)
15
- }
16
- return file === p
17
- })
18
- }
19
-
20
- /**
21
- * Default layout
22
- * @param {Object} props
23
- * @param {string|undefined} props.title
24
- * @param {import('react').ReactNode} props.children
25
- * @returns {React.JSX.Element}
26
- */
27
- export function DefaultLayout({ title, children }) {
28
- return createElement(
29
- 'html',
30
- { lang: 'en' },
31
- createElement(
32
- 'head',
33
- {},
34
- createElement('meta', { charSet: 'utf-8' }),
35
- createElement('meta', {
36
- name: 'viewport',
37
- content: 'width=device-width, initial-scale=1'
38
- }),
39
- title && createElement('title', {}, title)
40
- ),
41
- createElement('body', {}, children)
42
- )
43
- }
44
-
45
1
  /**
46
2
  * @param {string} str
47
3
  * @returns {string}
@@ -53,21 +9,3 @@ export function escapeHtml(str) {
53
9
  .replace(/</g, '&lt;')
54
10
  .replace(/>/g, '&gt;')
55
11
  }
56
-
57
- /**
58
- * @param {string} cmd
59
- */
60
- export async function $(cmd) {
61
- return new Promise((resolve, reject) => {
62
- exec(cmd, (err, stdout, stderr) => {
63
- if (err) {
64
- reject(err)
65
- }
66
- if (stderr) {
67
- console.error(stderr)
68
- }
69
- console.log(stdout)
70
- resolve(stdout)
71
- })
72
- })
73
- }
package/lib/vite.d.ts CHANGED
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * @param {Object} options
3
3
  * @param {string} options.dir
4
- * @param {string} [options.outDir]
5
- * @param {string[]} [options.styles]
6
4
  * @param {string[]} [options.exclude]
5
+ * @param {string | undefined} [options.site] - Absolute site URL, exported as `site`
7
6
  * @returns {import('vite').Plugin}
8
7
  */
9
- export function pluginFactory({ dir, outDir, styles, exclude }: {
8
+ export function pluginFactory({ dir, exclude, site }: {
10
9
  dir: string;
11
- outDir?: string | undefined;
12
- styles?: string[] | undefined;
13
10
  exclude?: string[] | undefined;
11
+ site?: string | undefined;
14
12
  }): import("vite").Plugin;
15
- export const appEntryId: "/@orga-build/main.js";
13
+ /** URL the dev server serves the island runtime from. */
14
+ export const islandRuntimeId: "/@orga-build/islands.js";
16
15
  //# sourceMappingURL=vite.d.ts.map
package/lib/vite.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["vite.js"],"names":[],"mappings":"AAWA;;;;;;;GAOG;AACH,gEANG;IAAwB,GAAG,EAAnB,MAAM;IACW,MAAM;IACJ,MAAM;IACN,OAAO;CAClC,GAAU,OAAO,MAAM,EAAE,MAAM,CA4LjC;AAvMD,gDAAuD"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["vite.js"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,sDALG;IAAwB,GAAG,EAAnB,MAAM;IACa,OAAO;IACG,IAAI,GAAjC,MAAM,GAAG,SAAS;CAC1B,GAAU,OAAO,MAAM,EAAE,MAAM,CA8OjC;AAzPD,yDAAyD;AACzD,wDAA+D"}
package/lib/vite.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import path from 'node:path'
2
2
  import { setup } from './files.js'
3
+ import { clientRuntime } from './island.js'
3
4
 
4
5
  const magicModulePrefix = '/@orga-build/'
5
6
  const pagesModuleId = `${magicModulePrefix}pages`
6
7
  const endpointsModuleId = `${magicModulePrefix}endpoints`
7
- export const appEntryId = `${magicModulePrefix}main.js`
8
+ /** URL the dev server serves the island runtime from. */
9
+ export const islandRuntimeId = `${magicModulePrefix}islands.js`
8
10
  const contentModuleId = 'orga-build:content'
9
11
  const contentModuleIdResolved = `\0${contentModuleId}`
10
12
  const endpointModulePrefix = `${endpointsModuleId}/__route__/`
@@ -12,13 +14,27 @@ const endpointModulePrefix = `${endpointsModuleId}/__route__/`
12
14
  /**
13
15
  * @param {Object} options
14
16
  * @param {string} options.dir
15
- * @param {string} [options.outDir]
16
- * @param {string[]} [options.styles]
17
17
  * @param {string[]} [options.exclude]
18
+ * @param {string | undefined} [options.site] - Absolute site URL, exported as `site`
18
19
  * @returns {import('vite').Plugin}
19
20
  */
20
- export function pluginFactory({ dir, outDir, styles = [], exclude = [] }) {
21
- const files = setup(dir, { outDir, exclude })
21
+ export function pluginFactory({ dir, exclude = [], site }) {
22
+ /** @type {ReturnType<typeof setup>} */
23
+ let files
24
+ /** @type {import('vite').ViteDevServer | undefined} */
25
+ let server
26
+ const contentDir = path.resolve(dir)
27
+
28
+ /**
29
+ * Whether a changed file lives in the content root, skipping dot paths the
30
+ * same way route discovery does.
31
+ * @param {string} file
32
+ */
33
+ function isContentFile(file) {
34
+ const rel = path.relative(contentDir, file)
35
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false
36
+ return !rel.split(path.sep).some((segment) => segment.startsWith('.'))
37
+ }
22
38
 
23
39
  return {
24
40
  name: 'vite-plugin-orga-pages',
@@ -31,29 +47,59 @@ export function pluginFactory({ dir, outDir, styles = [], exclude = [] }) {
31
47
  }
32
48
  }),
33
49
 
50
+ configResolved(config) {
51
+ // Exclude the outDir Vite actually writes the site to (it may sit inside
52
+ // the content root), so generated files are never discovered as routes.
53
+ // Read it from the client environment: during a build, `config.build`
54
+ // is the current environment's (e.g. the SSR outDir).
55
+ const outDir = config.environments.client.build.outDir
56
+ files = setup(dir, {
57
+ outDir: path.resolve(config.root, outDir),
58
+ exclude,
59
+ // Preview drafts in dev; leave them out of the build.
60
+ drafts: config.command === 'serve'
61
+ })
62
+ },
63
+
34
64
  async configureServer(_server) {
65
+ server = _server
35
66
  // Eagerly run file discovery so route conflicts surface at startup
36
67
  await files.pages()
37
68
  await files.endpoints()
38
69
  },
39
70
 
40
- hotUpdate() {
71
+ hotUpdate({ file }) {
72
+ // Content changes affect routes; files the server renders affect pages.
73
+ // Anything else in the project (logs, editor backups, tool state) must
74
+ // not trigger a reload.
75
+ const rendered =
76
+ server?.environments.ssr.moduleGraph.getModulesByFile(file)
77
+ if (!isContentFile(file) && !rendered?.size) return
41
78
  // Invalidate in-memory file caches so added/removed routes are picked up
42
79
  files.invalidate()
43
- // Invalidate content module when content files change
44
- const module = this.environment.moduleGraph.getModuleById(
45
- contentModuleIdResolved
46
- )
47
- if (module) {
48
- this.environment.moduleGraph.invalidateModule(module)
49
- // Full reload for now; can optimize to HMR later
80
+ // The generated modules list files by path, so regenerate them all:
81
+ // otherwise added pages are missing and deleted layouts stay imported.
82
+ const { moduleGraph } = this.environment
83
+ for (const [id, module] of moduleGraph.idToModuleMap) {
84
+ if (
85
+ id === contentModuleIdResolved ||
86
+ id.startsWith(magicModulePrefix)
87
+ ) {
88
+ moduleGraph.invalidateModule(module)
89
+ }
90
+ }
91
+ // Pages render on the server: the browser has nothing to hot-swap,
92
+ // so reload it.
93
+ if (this.environment.name === 'client') {
50
94
  this.environment.hot.send({ type: 'full-reload', path: '*' })
51
95
  }
52
96
  },
53
97
 
54
98
  async resolveId(id, _importer) {
55
- if (id === appEntryId) {
56
- return appEntryId
99
+ // Pages with islands link the runtime by a stable URL; point it at the
100
+ // real file so Vite treats it like any other source module.
101
+ if (id === islandRuntimeId) {
102
+ return clientRuntime
57
103
  }
58
104
  if (id === contentModuleId) {
59
105
  return contentModuleIdResolved
@@ -63,12 +109,6 @@ export function pluginFactory({ dir, outDir, styles = [], exclude = [] }) {
63
109
  }
64
110
  },
65
111
  async load(id) {
66
- if (id === appEntryId) {
67
- const styleImports = styles
68
- .map((styleUrl) => `import ${JSON.stringify(styleUrl)};`)
69
- .join('\n')
70
- return `${styleImports}\nimport "orga-build/csr";`
71
- }
72
112
  if (id === contentModuleIdResolved) {
73
113
  return await renderContentModule()
74
114
  }
@@ -81,12 +121,21 @@ export function pluginFactory({ dir, outDir, styles = [], exclude = [] }) {
81
121
  if (id.startsWith(pagesModuleId)) {
82
122
  const pageId = id.replace(pagesModuleId, '')
83
123
  const page = await files.page(pageId)
84
- if (page) {
124
+ if (!page) return
125
+ // Org pages take `_components` as the `components` prop.
126
+ if (page.dataPath.endsWith('.org')) {
85
127
  return `
128
+ import { createElement } from 'react';
129
+ import Content from '${page.dataPath}';
130
+ import * as components from '${magicModulePrefix}components';
86
131
  export * from '${page.dataPath}';
87
- export {default} from '${page.dataPath}';
132
+ export default (props) => createElement(Content, { components: { ...components }, ...props });
88
133
  `
89
134
  }
135
+ return `
136
+ export * from '${page.dataPath}';
137
+ export {default} from '${page.dataPath}';
138
+ `
90
139
  }
91
140
  if (id.startsWith(endpointModulePrefix)) {
92
141
  const routeHex = id.slice(endpointModulePrefix.length)
@@ -172,6 +221,8 @@ export default layouts;
172
221
  return `
173
222
  const __entries = ${manifest}
174
223
 
224
+ export const site = ${JSON.stringify(site)}
225
+
175
226
  function normalizePath(path = '') {
176
227
  return String(path).replace(/^\\/+|\\/+$/g, '')
177
228
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "orga-build",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A simple tool that builds org-mode files into a website",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=20.19.0"
7
+ "node": "^20.19.0 || >=22.12.0"
8
8
  },
9
9
  "bin": {
10
10
  "orga-build": "./cli.js"
@@ -22,8 +22,6 @@
22
22
  "types": "./index.d.ts",
23
23
  "import": "./index.js"
24
24
  },
25
- "./csr": "./lib/csr.jsx",
26
- "./components": "./lib/components.js",
27
25
  "./client": {
28
26
  "types": "./lib/content.d.ts"
29
27
  }
@@ -43,16 +41,15 @@
43
41
  "directory": "packages/orga-build"
44
42
  },
45
43
  "dependencies": {
46
- "@vitejs/plugin-react": "^5.1.4",
44
+ "@vitejs/plugin-react": "^6.1.2",
47
45
  "globby": "^14.1.0",
48
46
  "react": "^19.0.0",
49
47
  "react-dom": "^19.0.0",
50
48
  "rehype-katex": "^7.0.1",
51
49
  "unified": "^11.0.5",
52
50
  "unist-util-visit-parents": "^6.0.1",
53
- "vite": "^7.3.1",
54
- "wouter": "^3.7.0",
55
- "@orgajs/rollup": "1.3.4"
51
+ "vite": "^8.3.3",
52
+ "@orgajs/rollup": "1.3.5"
56
53
  },
57
54
  "devDependencies": {
58
55
  "@types/hast": "^3.0.4",
@@ -1,2 +0,0 @@
1
- export { Link } from "wouter";
2
- //# sourceMappingURL=components.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["components.js"],"names":[],"mappings":""}
package/lib/components.js DELETED
@@ -1 +0,0 @@
1
- export { Link } from 'wouter'
package/lib/csr.jsx DELETED
@@ -1,11 +0,0 @@
1
- import { createRoot } from 'react-dom/client'
2
- import { Router } from 'wouter'
3
- import { App } from './app.jsx'
4
-
5
- const container = document.getElementById('root')
6
- const root = createRoot(container)
7
- root.render(
8
- <Router>
9
- <App />
10
- </Router>
11
- )
package/lib/watch.d.ts DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * @param {import("fs").PathLike} dir
3
- * @param {RegExp} ignore
4
- * @param {{(event: fs.FileChangeInfo<string>): Promise<void> | void}} onChange
5
- */
6
- export function watch(dir: import("fs").PathLike, ignore: RegExp, onChange: {
7
- (event: fs.FileChangeInfo<string>): Promise<void> | void;
8
- }): Promise<void>;
9
- import fs from 'node:fs/promises';
10
- //# sourceMappingURL=watch.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["watch.js"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,2BAJW,OAAO,IAAI,EAAE,QAAQ,UACrB,MAAM,YACN;IAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAAC,iBA+CpE;eApDc,kBAAkB"}
package/lib/watch.js DELETED
@@ -1,53 +0,0 @@
1
- import fs from 'node:fs/promises'
2
-
3
- /**
4
- * @param {import("fs").PathLike} dir
5
- * @param {RegExp} ignore
6
- * @param {{(event: fs.FileChangeInfo<string>): Promise<void> | void}} onChange
7
- */
8
- export async function watch(dir, ignore, onChange) {
9
- let busy = false
10
- let dirty = false
11
- /** @type {ReturnType<typeof setTimeout> | null} */
12
- let timeout = null
13
- const delay = 1000
14
- const defaultIgnorePattern = /node_modules|\.git|\.DS_Store|\.orga-build/
15
-
16
- const watcher = fs.watch(dir, { recursive: true })
17
- for await (const event of watcher) {
18
- if (
19
- event.eventType !== 'change' ||
20
- event.filename === null ||
21
- shouldIgnore(event.filename)
22
- ) {
23
- continue
24
- }
25
- console.log(`file changed: ${event.filename}`)
26
- dirty = true
27
- if (busy) {
28
- continue
29
- }
30
- if (timeout !== null) clearTimeout(timeout)
31
- timeout = setTimeout(processEvent, delay, event)
32
- }
33
-
34
- /**
35
- * @param {any} event
36
- */
37
- async function processEvent(event) {
38
- busy = true
39
- dirty = false
40
- await onChange(event)
41
- busy = false
42
- if (dirty) {
43
- processEvent(event)
44
- }
45
- }
46
-
47
- /**
48
- * @param {string} filename
49
- */
50
- function shouldIgnore(filename) {
51
- return defaultIgnorePattern.test(filename) || ignore.test(filename)
52
- }
53
- }