orga-build 0.4.0 → 0.5.1

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 CHANGED
@@ -2,12 +2,96 @@
2
2
 
3
3
  A simple tool that builds org-mode files into a website.
4
4
 
5
+ * Architecture
6
+
7
+ orga-build is built on top of Vite with a Vite-native architecture. The dev server uses Vite's native =createServer().listen()= pattern (no custom Express wrapper), which ensures maximum compatibility with the Vite ecosystem, including plugins like Cloudflare Workers.
8
+
9
+ ** Key Design Principles
10
+
11
+ - *Vite-native*: Framework behavior is implemented as Vite plugins, not external server wrappers
12
+ - *Zero-config*: Works with just =.org= files - no =index.html= required
13
+ - *Plugin-driven*: All features are implemented as composable Vite plugins
14
+
15
+ ** Plugin Composition
16
+
17
+ When using orga-build CLI with additional Vite plugins (like Cloudflare Workers), add them to =vitePlugins= in your =orga.config.js=:
18
+
19
+ #+begin_src javascript
20
+ // orga.config.js
21
+ import { cloudflare } from '@cloudflare/vite-plugin'
22
+
23
+ export default {
24
+ root: 'pages',
25
+ // Add external plugins here - orga-build plugins are included automatically
26
+ vitePlugins: [
27
+ cloudflare()
28
+ ]
29
+ }
30
+ #+end_src
31
+
32
+ Note: Do NOT add =orgaBuildPlugin()= to =vitePlugins= - it's already included by the CLI. Only add external plugins.
33
+
34
+ For advanced users integrating directly with Vite (without the orga-build CLI), use =orgaBuildPlugin= in your =vite.config.js=:
35
+
36
+ #+begin_src javascript
37
+ // vite.config.js (advanced - direct Vite integration)
38
+ import { cloudflare } from '@cloudflare/vite-plugin'
39
+ import { orgaBuildPlugin, alias } from 'orga-build'
40
+
41
+ export default {
42
+ plugins: [
43
+ cloudflare(),
44
+ ...orgaBuildPlugin({ root: 'pages', containerClass: [] })
45
+ ],
46
+ resolve: { alias }
47
+ }
48
+ #+end_src
49
+
50
+ ** Default HTML Template
51
+
52
+ If your project doesn't have an =index.html= file, orga-build provides a default template that:
53
+ - Sets up React rendering
54
+ - Enables client-side routing
55
+ - Works in both dev and production builds
56
+
57
+ To customize the HTML shell, create your own =index.html= in your project root.
58
+
5
59
  * Installation
6
60
 
7
61
  #+begin_src bash
8
62
  npm install orga-build
9
63
  #+end_src
10
64
 
65
+ * Configuration
66
+
67
+ orga-build uses =orga.config.js= (or =orga.config.mjs=) as the primary configuration file. This file should be placed in your project root.
68
+
69
+ #+begin_src javascript
70
+ // orga.config.js
71
+ export default {
72
+ // Directory containing your .org files (default: 'pages')
73
+ root: 'pages',
74
+
75
+ // Output directory for production build (default: 'out')
76
+ outDir: 'out',
77
+
78
+ // CSS class(es) to wrap rendered org content
79
+ containerClass: ['prose', 'prose-lg'],
80
+
81
+ // Additional Vite plugins
82
+ vitePlugins: []
83
+ }
84
+ #+end_src
85
+
86
+ ** Configuration Options
87
+
88
+ | Option | Type | Default | Description |
89
+ |--------+------+---------+-------------|
90
+ | =root= | =string= | ='pages'= | Directory containing content files |
91
+ | =outDir= | =string= | ='out'= | Output directory for production build |
92
+ | =containerClass= | =string \vert string[]= | =[]= | CSS class(es) for content wrapper |
93
+ | =vitePlugins= | =PluginOption[]= | =[]= | Additional Vite plugins |
94
+
11
95
  * TypeScript Setup
12
96
 
13
97
  If you're using TypeScript and want type support for the =orga-build:content= virtual module, you need to add a reference to the type definitions.
package/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { argv, cwd } from 'node:process'
3
+ import { argv } from 'node:process'
4
4
  import { parseArgs } from 'node:util'
5
5
  import { loadConfig } from './lib/config.js'
6
6
  import { build } from './lib/build.js'
@@ -10,12 +10,12 @@ const { positionals } = parseArgs({
10
10
  args: argv.slice(2),
11
11
  options: {
12
12
  watch: { type: 'boolean', short: 'w' },
13
- outDir: { type: 'string', short: 'o', default: 'out' }
13
+ outDir: { type: 'string', short: 'o', default: '.out' }
14
14
  },
15
15
  tokens: true,
16
16
  allowPositionals: true
17
17
  })
18
18
 
19
- const config = await loadConfig(cwd(), 'orga.config.js', 'orga.config.mjs')
19
+ const config = await loadConfig('orga.config.js', 'orga.config.mjs')
20
20
 
21
21
  await (positionals.includes('dev') ? serve(config) : build(config))
package/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { build } from "./lib/build.js";
2
+ export { orgaBuildPlugin, createOrgaBuildConfig, alias } from "./lib/plugin.js";
2
3
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { build } from './lib/build.js'
2
+ export { orgaBuildPlugin, createOrgaBuildConfig, alias } from './lib/plugin.js'
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=build.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.test.d.ts","sourceRoot":"","sources":["build.test.js"],"names":[],"mappings":""}
@@ -0,0 +1,64 @@
1
+ import { test, describe, before, after } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import fs from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { build } from '../build.js'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const fixtureDir = path.join(__dirname, 'fixtures')
10
+ const outDir = path.join(__dirname, '.test-output')
11
+
12
+ describe('orga-build', () => {
13
+ before(async () => {
14
+ await fs.mkdir(fixtureDir, { recursive: true })
15
+ // Create minimal fixture
16
+ await fs.writeFile(
17
+ path.join(fixtureDir, 'index.org'),
18
+ `#+title: Test Page
19
+
20
+ * Hello World
21
+
22
+ This is a test page.
23
+ `
24
+ )
25
+ })
26
+
27
+ after(async () => {
28
+ await fs.rm(outDir, { recursive: true, force: true })
29
+ await fs.rm(fixtureDir, { recursive: true, force: true })
30
+ })
31
+
32
+ test('builds org files to HTML', async () => {
33
+ await build({
34
+ root: fixtureDir,
35
+ outDir: outDir,
36
+ containerClass: [],
37
+ vitePlugins: [],
38
+ preBuild: [],
39
+ postBuild: []
40
+ })
41
+
42
+ // Check output exists
43
+ const indexPath = path.join(outDir, 'index.html')
44
+ const indexExists = await fs
45
+ .access(indexPath)
46
+ .then(() => true)
47
+ .catch(() => false)
48
+ assert.ok(indexExists, 'index.html should exist')
49
+
50
+ // Check content
51
+ const html = await fs.readFile(indexPath, 'utf-8')
52
+ assert.ok(html.includes('<title>Test Page</title>'), 'should have title')
53
+ assert.ok(html.includes('Hello World'), 'should have heading content')
54
+ })
55
+
56
+ test('generates assets directory', async () => {
57
+ const assetsDir = path.join(outDir, 'assets')
58
+ const assetsExists = await fs
59
+ .access(assetsDir)
60
+ .then(() => true)
61
+ .catch(() => false)
62
+ assert.ok(assetsExists, 'assets directory should exist')
63
+ })
64
+ })
package/lib/build.d.ts CHANGED
@@ -2,9 +2,6 @@
2
2
  * @param {import('./config.js').Config} config
3
3
  */
4
4
  export function build({ outDir, root, containerClass, vitePlugins }: import("./config.js").Config): Promise<void>;
5
- export const alias: {
6
- react: string;
7
- 'react-dom': string;
8
- wouter: string;
9
- };
5
+ export { alias };
6
+ import { alias } from './plugin.js';
10
7
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["build.js"],"names":[],"mappings":"AAmBA;;GAEG;AACH,qEAFW,OAAO,aAAa,EAAE,MAAM,iBA8JtC;AArKD;;;;EAIC"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["build.js"],"names":[],"mappings":"AAcA;;GAEG;AACH,qEAFW,OAAO,aAAa,EAAE,MAAM,iBAqJtC;;sBA/J4C,aAAa"}
package/lib/build.js CHANGED
@@ -1,21 +1,16 @@
1
1
  import path from 'node:path'
2
- import { createRequire } from 'node:module'
3
- import { build as viteBuild } from 'vite'
4
- import { setupOrga } from './orga.js'
5
- import react from '@vitejs/plugin-react'
2
+ import { createBuilder } from 'vite'
6
3
  import { fileURLToPath, pathToFileURL } from 'node:url'
7
- import { copy, emptyDir, ensureDir } from './fs.js'
8
- import { pluginFactory } from './vite.js'
4
+ import { copy, emptyDir, ensureDir, exists } from './fs.js'
9
5
  import fs from 'fs/promises'
10
- import assert from 'node:assert'
6
+ import { createOrgaBuildConfig, alias } from './plugin.js'
11
7
 
12
- const require = createRequire(import.meta.url)
8
+ // Re-export alias for backwards compatibility
9
+ export { alias }
13
10
 
14
- export const alias = {
15
- react: path.dirname(require.resolve('react/package.json')),
16
- 'react-dom': path.dirname(require.resolve('react-dom/package.json')),
17
- wouter: path.dirname(require.resolve('wouter'))
18
- }
11
+ const ssrEntry = fileURLToPath(new URL('./ssr.jsx', import.meta.url))
12
+ const clientEntry = fileURLToPath(new URL('./client.jsx', import.meta.url))
13
+ const defaultIndexHtml = fileURLToPath(new URL('./index.html', import.meta.url))
19
14
 
20
15
  /**
21
16
  * @param {import('./config.js').Config} config
@@ -26,98 +21,89 @@ export async function build({
26
21
  containerClass,
27
22
  vitePlugins = []
28
23
  }) {
29
- /* --- prepare folders, out, ssr, client --- */
30
24
  await emptyDir(outDir)
31
25
  const ssrOutDir = path.join(outDir, '.ssr')
32
26
  const clientOutDir = path.join(outDir, '.client')
33
27
 
34
- const plugins = [
35
- setupOrga({ containerClass }),
36
- react(),
37
- pluginFactory({ dir: root }),
38
- ...vitePlugins
39
- ]
28
+ const { plugins, resolve } = createOrgaBuildConfig({
29
+ root,
30
+ outDir,
31
+ containerClass,
32
+ vitePlugins
33
+ })
40
34
 
41
- /* --- build ssr bundle: server.mjs --- */
42
- console.log('preparing ssr bundle...')
43
- await viteBuild({
35
+ // Shared config with environment-specific build settings
36
+ const builder = await createBuilder({
44
37
  root,
45
38
  plugins,
46
- build: {
47
- ssr: true,
48
- cssCodeSplit: false,
49
- emptyOutDir: true,
50
- rollupOptions: {
51
- input: fileURLToPath(new URL('./ssr.jsx', import.meta.url)),
52
- output: {
53
- entryFileNames: '[name].mjs',
54
- chunkFileNames: '[name]-[hash].mjs'
39
+ resolve,
40
+ ssr: { noExternal: true },
41
+ environments: {
42
+ ssr: {
43
+ build: {
44
+ ssr: true,
45
+ outDir: ssrOutDir,
46
+ cssCodeSplit: false,
47
+ emptyOutDir: true,
48
+ minify: false,
49
+ rollupOptions: {
50
+ input: ssrEntry,
51
+ output: {
52
+ entryFileNames: '[name].mjs',
53
+ chunkFileNames: '[name]-[hash].mjs'
54
+ }
55
+ }
55
56
  }
56
57
  },
57
- outDir: ssrOutDir,
58
- minify: false
59
- },
60
- ssr: {
61
- noExternal: true
62
- },
63
- resolve: {
64
- alias: alias
58
+ client: {
59
+ build: {
60
+ outDir: clientOutDir,
61
+ cssCodeSplit: false,
62
+ emptyOutDir: true,
63
+ assetsDir: 'assets',
64
+ rollupOptions: {
65
+ input: clientEntry,
66
+ preserveEntrySignatures: 'allow-extension'
67
+ }
68
+ }
69
+ }
65
70
  }
66
71
  })
67
72
 
68
- /* --- import ssr bundle entry output, to get all the data and render function --- */
73
+ // Build SSR first to get render function and pages
74
+ console.log('preparing ssr bundle...')
75
+ await builder.build(builder.environments.ssr)
69
76
 
70
77
  const { render, pages } = await import(
71
78
  pathToFileURL(path.join(ssrOutDir, 'ssr.mjs')).toString()
72
79
  )
73
80
 
74
- /* --- build client bundle: client.mjs --- */
75
- const _clientResult = await viteBuild({
76
- root,
77
- plugins,
78
- build: {
79
- cssCodeSplit: false,
80
- emptyOutDir: true,
81
- rollupOptions: {
82
- input: fileURLToPath(new URL('./client.jsx', import.meta.url)),
83
- preserveEntrySignatures: 'allow-extension'
84
- },
85
- assetsDir: 'assets',
86
- outDir: clientOutDir
87
- },
88
- ssr: {
89
- noExternal: true
90
- },
91
- resolve: {
92
- alias: alias
93
- }
94
- })
81
+ // Build client bundle
82
+ const _clientResult = await builder.build(builder.environments.client)
95
83
 
96
- /** @type {import('vite').Rollup.RollupOutput} */
97
- let clientResult
98
- if (Array.isArray(_clientResult)) {
99
- if (_clientResult.length !== 1)
100
- throw new Error(`expect viteBuild to have only one BuildResult`)
101
- clientResult = _clientResult[0]
102
- } else {
103
- assert('output' in _clientResult)
104
- clientResult = _clientResult
105
- }
84
+ // Normalize build result to single RollupOutput
85
+ const clientOutput = Array.isArray(_clientResult)
86
+ ? _clientResult[0].output
87
+ : 'output' in _clientResult
88
+ ? _clientResult.output
89
+ : null
90
+ if (!clientOutput) throw new Error('Unexpected client build result')
106
91
 
107
92
  /* --- get from client bundle result: entry chunk, css chunks --- */
108
- const entryChunk = clientResult.output.filter((c) => {
109
- return c.type === 'chunk' && c.isEntry
110
- })[0]
93
+ const entryChunk = clientOutput.find(
94
+ (/** @type {any} */ c) => c.type === 'chunk' && c.isEntry
95
+ )
111
96
 
112
- const cssChunks = clientResult.output.filter((c) => {
113
- return c.type === 'asset' && c.fileName.endsWith('.css')
114
- })
97
+ const cssChunks = clientOutput.filter(
98
+ (/** @type {any} */ c) => c.type === 'asset' && c.fileName.endsWith('.css')
99
+ )
115
100
 
116
101
  /* --- get html template, inject entry js and css --- */
117
- const template = await fs.readFile(
118
- fileURLToPath(new URL('./index.html', import.meta.url)),
119
- { encoding: 'utf-8' }
120
- )
102
+ // Check for user's index.html in project root, otherwise use default
103
+ const projectRoot = process.cwd()
104
+ const userIndexPath = path.join(projectRoot, 'index.html')
105
+ const indexHtmlPath = (await exists(userIndexPath)) ? userIndexPath : defaultIndexHtml
106
+ const template = await fs.readFile(indexHtmlPath, { encoding: 'utf-8' })
121
107
  /* --- for each page path, render html using render function from ssr bundle, and inject the right css --- */
122
108
  const pagePaths = Object.keys(pages)
123
109
  await Promise.all(
@@ -157,11 +143,11 @@ export async function build({
157
143
  `
158
144
  )
159
145
  const css = cssChunks
160
- .map((c) => `<link rel="stylesheet" href="/${c.fileName}">`)
146
+ .map((/** @type {any} */ c) => `<link rel="stylesheet" href="/${c.fileName}">`)
161
147
  .join('\n')
162
148
  html = html.replace(
163
149
  '<script type="module" src="/@orga-build/main.js"></script>',
164
- `<script type="module" src="/${entryChunk.fileName}"></script>`
150
+ `<script type="module" src="/${entryChunk?.fileName}"></script>`
165
151
  )
166
152
 
167
153
  html = html.replace('</head>', `${css}</head>`)
package/lib/config.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * @param {string} cwd
3
2
  * @param {string[]} files
4
3
  * @returns {Promise<Config>}
5
4
  */
6
- export function loadConfig(cwd: string, ...files: string[]): Promise<Config>;
5
+ export function loadConfig(...files: string[]): Promise<Config>;
7
6
  export type Config = {
8
7
  outDir: string;
9
8
  root: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.js"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,gCAJW,MAAM,YACN,MAAM,EAAE,GACN,OAAO,CAAC,MAAM,CAAC,CAkB3B;;YAvCa,MAAM;UACN,MAAM;cACN,MAAM,EAAE;eACR,MAAM,EAAE;;;;iBACR,OAAO,MAAM,EAAE,YAAY,EAAE;oBAC7B,MAAM,EAAE,GAAC,MAAM"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.js"],"names":[],"mappings":"AAuBA;;;GAGG;AACH,qCAHW,MAAM,EAAE,GACN,OAAO,CAAC,MAAM,CAAC,CAsC3B;;YA1Da,MAAM;UACN,MAAM;cACN,MAAM,EAAE;eACR,MAAM,EAAE;;;;iBACR,OAAO,MAAM,EAAE,YAAY,EAAE;oBAC7B,MAAM,EAAE,GAAC,MAAM"}
package/lib/config.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
- import { resolvePath } from './fs.js'
4
3
 
5
4
  /**
6
5
  * @typedef {Object} Config
@@ -14,8 +13,8 @@ import { resolvePath } from './fs.js'
14
13
 
15
14
  /** @type {Config} */
16
15
  const defaultConfig = {
17
- outDir: 'out',
18
- root: 'pages',
16
+ outDir: '.out',
17
+ root: '.',
19
18
  preBuild: [],
20
19
  postBuild: [],
21
20
  vitePlugins: [],
@@ -23,24 +22,43 @@ const defaultConfig = {
23
22
  }
24
23
 
25
24
  /**
26
- * @param {string} cwd
27
25
  * @param {string[]} files
28
26
  * @returns {Promise<Config>}
29
27
  */
30
- export async function loadConfig(cwd, ...files) {
28
+ export async function loadConfig(...files) {
29
+ const cwd = process.cwd()
30
+
31
+ /**
32
+ * @param {string} value
33
+ */
34
+ const resolveConfigPath = (value) =>
35
+ path.isAbsolute(value) ? value : path.resolve(cwd, value)
36
+
37
+ let result = { ...defaultConfig }
38
+
31
39
  for (const file of files) {
32
40
  const filePath = path.join(cwd, file)
33
41
 
34
42
  try {
35
43
  await fs.access(filePath, fs.constants.F_OK)
36
- const config = await import(filePath)
37
- const result = { ...defaultConfig, ...config }
38
- result.root = resolvePath(result.root)
39
- result.outDir = resolvePath(result.outDir)
40
- return result
44
+ } catch {
45
+ // File doesn't exist, try next
46
+ continue
47
+ }
48
+
49
+ try {
50
+ const module = await import(filePath)
51
+ // Support both default export (recommended) and named exports
52
+ const config = module.default || module
53
+ result = { ...defaultConfig, ...config }
54
+ break
41
55
  } catch (err) {
42
- console.error(err)
56
+ // Config file exists but has errors
57
+ console.error(`Error loading config from ${file}:`, err)
43
58
  }
44
59
  }
45
- return defaultConfig
60
+
61
+ result.root = resolveConfigPath(result.root)
62
+ result.outDir = resolveConfigPath(result.outDir)
63
+ return result
46
64
  }
package/lib/files.d.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * @param {string} dir
3
+ * @param {object} [options]
4
+ * @param {string} [options.outDir] - Output directory to exclude from file discovery
3
5
  */
4
- export function setup(dir: string): {
6
+ export function setup(dir: string, { outDir }?: {
7
+ outDir?: string | undefined;
8
+ }): {
5
9
  pages: () => Promise<Record<string, Page>>;
6
10
  page: (id: string) => Promise<Page>;
7
11
  components: () => Promise<string | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.js"],"names":[],"mappings":"AA8EA;;GAEG;AACH,2BAFW,MAAM;;eA2HJ,MAAM;;;;EAKlB;;cAxMa,MAAM;;;;YACN,MAAM;;;QAMN,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":"AA8EA;;;;GAIG;AACH,2BAJW,MAAM,eAEd;IAAyB,MAAM;CACjC;;eAiIY,MAAM;;;;EAKlB;;cAjNa,MAAM;;;;YACN,MAAM;;;QAMN,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
@@ -78,8 +78,17 @@ function getContentId(slug) {
78
78
 
79
79
  /**
80
80
  * @param {string} dir
81
+ * @param {object} [options]
82
+ * @param {string} [options.outDir] - Output directory to exclude from file discovery
81
83
  */
82
- export function setup(dir) {
84
+ export function setup(dir, { outDir } = {}) {
85
+ const outDirRelative = outDir ? path.relative(dir, outDir) : null
86
+ // Only exclude outDir if it's inside the root (not an external path like ../out)
87
+ const outDirExclude =
88
+ outDirRelative && !outDirRelative.startsWith('..')
89
+ ? `!${outDirRelative}/**`
90
+ : null
91
+
83
92
  const pages = cache(async function () {
84
93
  const files = await globby(
85
94
  [
@@ -89,7 +98,7 @@ export function setup(dir) {
89
98
  '!**/.*/**',
90
99
  '!**/.*',
91
100
  '!node_modules/**',
92
- '!out/**'
101
+ ...(outDirExclude ? [outDirExclude] : [])
93
102
  ],
94
103
  { cwd: dir }
95
104
  )
@@ -113,7 +122,7 @@ export function setup(dir) {
113
122
  '!**/.*/**',
114
123
  '!**/.*',
115
124
  '!node_modules/**',
116
- '!out/**'
125
+ ...(outDirExclude ? [outDirExclude] : [])
117
126
  ],
118
127
  {
119
128
  cwd: dir
@@ -137,7 +146,7 @@ export function setup(dir) {
137
146
  '!**/.*/**',
138
147
  '!**/.*',
139
148
  '!node_modules/**',
140
- '!out/**'
149
+ ...(outDirExclude ? [outDirExclude] : [])
141
150
  ],
142
151
  {
143
152
  cwd: dir
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @typedef {Object} OrgaBuildPluginOptions
3
+ * @property {string} root - Root directory for content files
4
+ * @property {string | undefined} [outDir] - Output directory (excluded from file discovery)
5
+ * @property {string|string[]} [containerClass] - CSS class(es) to wrap rendered content
6
+ */
7
+ /**
8
+ * Creates the canonical orga-build plugin preset.
9
+ * This is the single composition path used by both dev and build.
10
+ *
11
+ * @param {OrgaBuildPluginOptions} options
12
+ * @returns {import('vite').PluginOption[]}
13
+ */
14
+ export function orgaBuildPlugin({ root, outDir, containerClass }: OrgaBuildPluginOptions): import("vite").PluginOption[];
15
+ /**
16
+ * Creates the full Vite config options for orga-build.
17
+ * Includes plugins, resolve aliases, and other shared config.
18
+ *
19
+ * @param {OrgaBuildPluginOptions & { outDir?: string, vitePlugins?: import('vite').PluginOption[], includeFallbackHtml?: boolean, projectRoot?: string }} options
20
+ * @returns {{ plugins: import('vite').PluginOption[], resolve: { alias: typeof alias } }}
21
+ */
22
+ export function createOrgaBuildConfig({ root, outDir, containerClass, vitePlugins, includeFallbackHtml, projectRoot }: OrgaBuildPluginOptions & {
23
+ outDir?: string;
24
+ vitePlugins?: import("vite").PluginOption[];
25
+ includeFallbackHtml?: boolean;
26
+ projectRoot?: string;
27
+ }): {
28
+ plugins: import("vite").PluginOption[];
29
+ resolve: {
30
+ alias: typeof alias;
31
+ };
32
+ };
33
+ /**
34
+ * Creates an HTML serving plugin that handles index.html for dev mode.
35
+ *
36
+ * This plugin:
37
+ * - Serves user's index.html from project root if present, otherwise uses the default template
38
+ * - Only handles GET/HEAD requests that accept HTML
39
+ * - Runs late (post middleware) so other plugins get first chance
40
+ * - Passes HTML through transformIndexHtml for ecosystem compatibility
41
+ * - Does not intercept asset requests
42
+ *
43
+ * @param {string} projectRoot - Project root directory (where orga.config.js lives)
44
+ * @returns {import('vite').Plugin}
45
+ */
46
+ export function htmlFallbackPlugin(projectRoot: string): import("vite").Plugin;
47
+ /**
48
+ * Alias map for React and wouter to ensure consistent resolution
49
+ */
50
+ export const alias: {
51
+ react: string;
52
+ 'react-dom': string;
53
+ wouter: string;
54
+ };
55
+ export type OrgaBuildPluginOptions = {
56
+ /**
57
+ * - Root directory for content files
58
+ */
59
+ root: string;
60
+ /**
61
+ * - Output directory (excluded from file discovery)
62
+ */
63
+ outDir?: string | undefined;
64
+ /**
65
+ * - CSS class(es) to wrap rendered content
66
+ */
67
+ containerClass?: string | string[];
68
+ };
69
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["plugin.js"],"names":[],"mappings":"AAoBA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,kEAHW,sBAAsB,GACpB,OAAO,MAAM,EAAE,YAAY,EAAE,CAIzC;AAED;;;;;;GAMG;AACH,uHAHW,sBAAsB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,CAAC;IAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5I;IAAE,OAAO,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,CAAC;IAAC,OAAO,EAAE;QAAE,KAAK,EAAE,OAAO,KAAK,CAAA;KAAE,CAAA;CAAE,CAoBxF;AAiBD;;;;;;;;;;;;GAYG;AACH,gDAHW,MAAM,GACJ,OAAO,MAAM,EAAE,MAAM,CA8CjC;AA9HD;;GAEG;AACH;;;;EAIC;;;;;UAIa,MAAM;;;;aACN,MAAM,GAAG,SAAS;;;;qBAClB,MAAM,GAAC,MAAM,EAAE"}
package/lib/plugin.js ADDED
@@ -0,0 +1,138 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs/promises'
3
+ import { createRequire } from 'node:module'
4
+ import { fileURLToPath } from 'node:url'
5
+ import react from '@vitejs/plugin-react'
6
+ import { setupOrga } from './orga.js'
7
+ import { pluginFactory } from './vite.js'
8
+
9
+ const require = createRequire(import.meta.url)
10
+ const defaultIndexHtml = fileURLToPath(new URL('./index.html', import.meta.url))
11
+
12
+ /**
13
+ * Alias map for React and wouter to ensure consistent resolution
14
+ */
15
+ export const alias = {
16
+ react: path.dirname(require.resolve('react/package.json')),
17
+ 'react-dom': path.dirname(require.resolve('react-dom/package.json')),
18
+ wouter: path.dirname(require.resolve('wouter'))
19
+ }
20
+
21
+ /**
22
+ * @typedef {Object} OrgaBuildPluginOptions
23
+ * @property {string} root - Root directory for content files
24
+ * @property {string | undefined} [outDir] - Output directory (excluded from file discovery)
25
+ * @property {string|string[]} [containerClass] - CSS class(es) to wrap rendered content
26
+ */
27
+
28
+ /**
29
+ * Creates the canonical orga-build plugin preset.
30
+ * This is the single composition path used by both dev and build.
31
+ *
32
+ * @param {OrgaBuildPluginOptions} options
33
+ * @returns {import('vite').PluginOption[]}
34
+ */
35
+ export function orgaBuildPlugin({ root, outDir, containerClass = [] }) {
36
+ return [setupOrga({ containerClass }), react(), pluginFactory({ dir: root, outDir })]
37
+ }
38
+
39
+ /**
40
+ * Creates the full Vite config options for orga-build.
41
+ * Includes plugins, resolve aliases, and other shared config.
42
+ *
43
+ * @param {OrgaBuildPluginOptions & { outDir?: string, vitePlugins?: import('vite').PluginOption[], includeFallbackHtml?: boolean, projectRoot?: string }} options
44
+ * @returns {{ plugins: import('vite').PluginOption[], resolve: { alias: typeof alias } }}
45
+ */
46
+ export function createOrgaBuildConfig({
47
+ root,
48
+ outDir,
49
+ containerClass = [],
50
+ vitePlugins = [],
51
+ includeFallbackHtml = false,
52
+ projectRoot = process.cwd()
53
+ }) {
54
+ const plugins = [...vitePlugins, ...orgaBuildPlugin({ root, outDir, containerClass })]
55
+ if (includeFallbackHtml) {
56
+ // HTML fallback must be first so it can handle HTML navigation requests
57
+ // before runtime plugins (e.g. Cloudflare) potentially return 404.
58
+ plugins.unshift(htmlFallbackPlugin(projectRoot))
59
+ }
60
+ return {
61
+ plugins,
62
+ resolve: { alias }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Checks if a user-provided index.html exists in the project root.
68
+ *
69
+ * @param {string} root - Project root directory
70
+ * @returns {Promise<boolean>}
71
+ */
72
+ async function hasUserIndexHtml(root) {
73
+ try {
74
+ await fs.access(path.join(root, 'index.html'), fs.constants.F_OK)
75
+ return true
76
+ } catch {
77
+ return false
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Creates an HTML serving plugin that handles index.html for dev mode.
83
+ *
84
+ * This plugin:
85
+ * - Serves user's index.html from project root if present, otherwise uses the default template
86
+ * - Only handles GET/HEAD requests that accept HTML
87
+ * - Runs late (post middleware) so other plugins get first chance
88
+ * - Passes HTML through transformIndexHtml for ecosystem compatibility
89
+ * - Does not intercept asset requests
90
+ *
91
+ * @param {string} projectRoot - Project root directory (where orga.config.js lives)
92
+ * @returns {import('vite').Plugin}
93
+ */
94
+ export function htmlFallbackPlugin(projectRoot) {
95
+ return {
96
+ name: 'orga-build:html-fallback',
97
+
98
+ async configureServer(server) {
99
+ // Determine which index.html to use at startup
100
+ // Look for user's index.html in project root (where orga.config.js lives)
101
+ const userIndexPath = path.join(projectRoot, 'index.html')
102
+ const userHasIndex = await hasUserIndexHtml(projectRoot)
103
+ const indexHtmlPath = userHasIndex ? userIndexPath : defaultIndexHtml
104
+
105
+ // Add middleware to serve HTML early in the chain.
106
+ server.middlewares.use(async (req, res, next) => {
107
+ // Only handle GET/HEAD requests
108
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
109
+ return next()
110
+ }
111
+
112
+ // Only handle browser-like navigation requests.
113
+ // Don't match generic */* accepts to avoid hijacking API requests.
114
+ const accept = req.headers.accept || ''
115
+ if (!accept.includes('text/html')) {
116
+ return next()
117
+ }
118
+
119
+ // Don't intercept asset requests (files with extensions)
120
+ const url = req.url || '/'
121
+ const pathname = url.split('?')[0]
122
+ if (pathname !== '/' && /\.\w+$/.test(pathname)) {
123
+ return next()
124
+ }
125
+
126
+ try {
127
+ let html = await fs.readFile(indexHtmlPath, 'utf-8')
128
+ html = await server.transformIndexHtml(url, html)
129
+ res.statusCode = 200
130
+ res.setHeader('Content-Type', 'text/html')
131
+ res.end(html)
132
+ } catch (e) {
133
+ next(e)
134
+ }
135
+ })
136
+ }
137
+ }
138
+ }
package/lib/serve.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  /**
2
+ * Start the development server using native Vite.
3
+ *
2
4
  * @param {import('./config.js').Config} config
3
5
  * @param {number} [port]
4
6
  */
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["serve.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH,8BAHW,OAAO,aAAa,EAAE,MAAM,SAC5B,MAAM,iBA6ChB"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["serve.js"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,8BAHW,OAAO,aAAa,EAAE,MAAM,SAC5B,MAAM,iBA2BhB"}
package/lib/serve.js CHANGED
@@ -1,56 +1,36 @@
1
- import express from 'express'
1
+ import path from 'node:path'
2
2
  import { createServer } from 'vite'
3
- import fs from 'node:fs/promises'
4
- import react from '@vitejs/plugin-react'
5
- import { pluginFactory } from './vite.js'
6
- import { alias } from './build.js'
7
- import { setupOrga } from './orga.js'
3
+ import { createOrgaBuildConfig } from './plugin.js'
8
4
 
9
5
  /**
6
+ * Start the development server using native Vite.
7
+ *
10
8
  * @param {import('./config.js').Config} config
11
9
  * @param {number} [port]
12
10
  */
13
11
  export async function serve(config, port = 3000) {
14
- const app = express()
15
- const vite = await createServer({
16
- plugins: [
17
- setupOrga({ containerClass: config.containerClass }),
18
- react(),
19
- pluginFactory({ dir: config.root }),
20
- ...config.vitePlugins
21
- ],
22
- server: { middlewareMode: true },
23
- appType: 'custom',
24
- resolve: {
25
- alias: alias
26
- }
27
- })
28
-
29
- app.use(vite.middlewares)
30
- app.get('/favicon.ico', (req, res) => {
31
- res.status(404).end()
12
+ const { plugins, resolve } = createOrgaBuildConfig({
13
+ root: config.root,
14
+ outDir: config.outDir,
15
+ containerClass: config.containerClass,
16
+ vitePlugins: config.vitePlugins,
17
+ includeFallbackHtml: true
32
18
  })
33
19
 
34
- app.use(async (req, res, next) => {
35
- const url = req.originalUrl
36
- if (req.method !== 'GET' || !req.headers.accept?.includes('text/html')) {
37
- return next()
38
- }
39
-
40
- try {
41
- // read index.html file from path relative to this file
42
- const indexPath = new URL('./index.html', import.meta.url).pathname
43
- let template = await fs.readFile(indexPath, { encoding: 'utf-8' })
44
- template = await vite.transformIndexHtml(url, template)
45
- const html = template
46
- res.status(200).setHeader('Content-Type', 'text/html').end(html)
47
- } catch (/** @type{any} */ e) {
48
- vite.ssrFixStacktrace(e)
49
- next(e)
20
+ const server = await createServer({
21
+ root: config.root,
22
+ plugins,
23
+ resolve,
24
+ appType: 'custom',
25
+ server: {
26
+ port,
27
+ strictPort: false,
28
+ watch: {
29
+ ignored: [path.resolve(config.outDir) + '/**']
30
+ }
50
31
  }
51
32
  })
52
33
 
53
- app.listen(port, () => {
54
- console.log(` Server running at http://localhost:${port}/`)
55
- })
34
+ await server.listen()
35
+ server.printUrls()
56
36
  }
package/lib/vite.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * @param {Object} options
3
3
  * @param {string} options.dir
4
+ * @param {string} [options.outDir]
4
5
  * @returns {import('vite').Plugin}
5
6
  */
6
- export function pluginFactory({ dir }: {
7
+ export function pluginFactory({ dir, outDir }: {
7
8
  dir: string;
9
+ outDir?: string | undefined;
8
10
  }): import("vite").Plugin;
9
11
  //# 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":"AASA;;;;GAIG;AACH,uCAHG;IAAwB,GAAG,EAAnB,MAAM;CACd,GAAU,OAAO,MAAM,EAAE,MAAM,CA6JjC"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["vite.js"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,+CAJG;IAAwB,GAAG,EAAnB,MAAM;IACW,MAAM;CAC/B,GAAU,OAAO,MAAM,EAAE,MAAM,CA2JjC"}
package/lib/vite.js CHANGED
@@ -10,15 +10,21 @@ const contentModuleIdResolved = '\0' + contentModuleId
10
10
  /**
11
11
  * @param {Object} options
12
12
  * @param {string} options.dir
13
+ * @param {string} [options.outDir]
13
14
  * @returns {import('vite').Plugin}
14
15
  */
15
- export function pluginFactory({ dir }) {
16
- const files = setup(dir)
16
+ export function pluginFactory({ dir, outDir }) {
17
+ const files = setup(dir, { outDir })
17
18
 
18
19
  return {
19
20
  name: 'vite-plugin-orga-pages',
20
21
  enforce: 'pre',
21
22
  config: (config, env) => ({
23
+ future: {
24
+ removePluginHookSsrArgument: 'warn',
25
+ removePluginHookHandleHotUpdate: 'warn',
26
+ removeSsrLoadModule: 'warn'
27
+ },
22
28
  optimizeDeps: {
23
29
  include: [
24
30
  'react',
@@ -31,25 +37,18 @@ export function pluginFactory({ dir }) {
31
37
  }
32
38
  }),
33
39
 
34
- configureServer(server) {
35
- const { watcher, moduleGraph, ws } = server
36
-
37
- // Invalidate content module on file changes
38
- watcher.on('change', (filePath) => {
39
- const module = moduleGraph.getModuleById(contentModuleIdResolved)
40
- if (module) {
41
- moduleGraph.invalidateModule(module)
42
- // Send HMR update to client
43
- ws.send({
44
- type: 'full-reload',
45
- path: '*'
46
- })
47
- }
48
- })
40
+ hotUpdate() {
41
+ // Invalidate content module when content files change
42
+ const module = this.environment.moduleGraph.getModuleById(
43
+ contentModuleIdResolved
44
+ )
45
+ if (module) {
46
+ this.environment.moduleGraph.invalidateModule(module)
47
+ // Full reload for now; can optimize to HMR later
48
+ this.environment.hot.send({ type: 'full-reload', path: '*' })
49
+ }
49
50
  },
50
51
 
51
- buildStart() {},
52
-
53
52
  async resolveId(id, importer) {
54
53
  if (id === appEntryId) {
55
54
  return appEntryId
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "orga-build",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "A simple tool that builds org-mode files into a website",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=20.19.0"
8
+ },
6
9
  "bin": {
7
10
  "orga-build": "./cli.js"
8
11
  },
@@ -41,7 +44,6 @@
41
44
  },
42
45
  "dependencies": {
43
46
  "@vitejs/plugin-react": "^5.1.4",
44
- "express": "^5.1.0",
45
47
  "globby": "^14.1.0",
46
48
  "react": "^19.0.0",
47
49
  "react-dom": "^19.0.0",
@@ -52,7 +54,6 @@
52
54
  "@orgajs/rollup": "1.3.3"
53
55
  },
54
56
  "devDependencies": {
55
- "@types/express": "^5.0.1",
56
57
  "@types/hast": "^3.0.4",
57
58
  "@types/node": "^22.13.1",
58
59
  "@types/react": "^19.0.8",
@@ -60,6 +61,7 @@
60
61
  "orga": "4.6.0"
61
62
  },
62
63
  "scripts": {
63
- "clean": "fd . -e d.ts -e d.ts.map -I -x rm {}"
64
+ "clean": "fd . -e d.ts -e d.ts.map -I -x rm {}",
65
+ "test": "node --test --no-warnings \"lib/__tests__/*.test.js\""
64
66
  }
65
67
  }