orga-build 0.5.0 → 0.5.2

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'
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":"AAqBA;;GAEG;AACH,qEAFW,OAAO,aAAa,EAAE,MAAM,iBAoJtC;AA9JD;;;;EAIC"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["build.js"],"names":[],"mappings":"AAcA;;GAEG;AACH,qEAFW,OAAO,aAAa,EAAE,MAAM,iBAmJtC;;sBA7J4C,aAAa"}
package/lib/build.js CHANGED
@@ -1,23 +1,16 @@
1
1
  import path from 'node:path'
2
- import { createRequire } from 'node:module'
3
2
  import { createBuilder } from 'vite'
4
- import { setupOrga } from './orga.js'
5
- import react from '@vitejs/plugin-react'
6
3
  import { fileURLToPath, pathToFileURL } from 'node:url'
7
- import { copy, emptyDir, ensureDir } from './fs.js'
8
- import { pluginFactory } from './vite.js'
4
+ import { emptyDir, ensureDir, exists } from './fs.js'
9
5
  import fs from 'fs/promises'
6
+ import { createOrgaBuildConfig, alias } from './plugin.js'
10
7
 
11
- const require = createRequire(import.meta.url)
12
-
13
- export const alias = {
14
- react: path.dirname(require.resolve('react/package.json')),
15
- 'react-dom': path.dirname(require.resolve('react-dom/package.json')),
16
- wouter: path.dirname(require.resolve('wouter'))
17
- }
8
+ // Re-export alias for backwards compatibility
9
+ export { alias }
18
10
 
19
11
  const ssrEntry = fileURLToPath(new URL('./ssr.jsx', import.meta.url))
20
12
  const clientEntry = fileURLToPath(new URL('./client.jsx', import.meta.url))
13
+ const defaultIndexHtml = fileURLToPath(new URL('./index.html', import.meta.url))
21
14
 
22
15
  /**
23
16
  * @param {import('./config.js').Config} config
@@ -30,20 +23,20 @@ export async function build({
30
23
  }) {
31
24
  await emptyDir(outDir)
32
25
  const ssrOutDir = path.join(outDir, '.ssr')
33
- const clientOutDir = path.join(outDir, '.client')
26
+ const clientOutDir = outDir
34
27
 
35
- const plugins = [
36
- setupOrga({ containerClass }),
37
- react(),
38
- pluginFactory({ dir: root }),
39
- ...vitePlugins
40
- ]
28
+ const { plugins, resolve } = createOrgaBuildConfig({
29
+ root,
30
+ outDir,
31
+ containerClass,
32
+ vitePlugins
33
+ })
41
34
 
42
35
  // Shared config with environment-specific build settings
43
36
  const builder = await createBuilder({
44
37
  root,
45
38
  plugins,
46
- resolve: { alias },
39
+ resolve,
47
40
  ssr: { noExternal: true },
48
41
  environments: {
49
42
  ssr: {
@@ -66,7 +59,7 @@ export async function build({
66
59
  build: {
67
60
  outDir: clientOutDir,
68
61
  cssCodeSplit: false,
69
- emptyOutDir: true,
62
+ emptyOutDir: false,
70
63
  assetsDir: 'assets',
71
64
  rollupOptions: {
72
65
  input: clientEntry,
@@ -106,10 +99,11 @@ export async function build({
106
99
  )
107
100
 
108
101
  /* --- get html template, inject entry js and css --- */
109
- const template = await fs.readFile(
110
- fileURLToPath(new URL('./index.html', import.meta.url)),
111
- { encoding: 'utf-8' }
112
- )
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' })
113
107
  /* --- for each page path, render html using render function from ssr bundle, and inject the right css --- */
114
108
  const pagePaths = Object.keys(pages)
115
109
  await Promise.all(
@@ -125,8 +119,6 @@ export async function build({
125
119
  })
126
120
  )
127
121
 
128
- await copy(clientOutDir, outDir)
129
- await fs.rm(clientOutDir, { recursive: true })
130
122
  await fs.rm(ssrOutDir, { recursive: true })
131
123
 
132
124
  return
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,iBA4ChB"}
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,55 +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
- 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
+ }
49
31
  }
50
32
  })
51
33
 
52
- app.listen(port, () => {
53
- console.log(` Server running at http://localhost:${port}/`)
54
- })
34
+ await server.listen()
35
+ server.printUrls()
55
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,CA2JjC"}
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,10 +10,11 @@ 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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orga-build",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "A simple tool that builds org-mode files into a website",
5
5
  "type": "module",
6
6
  "engines": {
@@ -44,7 +44,6 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@vitejs/plugin-react": "^5.1.4",
47
- "express": "^5.1.0",
48
47
  "globby": "^14.1.0",
49
48
  "react": "^19.0.0",
50
49
  "react-dom": "^19.0.0",
@@ -55,7 +54,6 @@
55
54
  "@orgajs/rollup": "1.3.3"
56
55
  },
57
56
  "devDependencies": {
58
- "@types/express": "^5.0.1",
59
57
  "@types/hast": "^3.0.4",
60
58
  "@types/node": "^22.13.1",
61
59
  "@types/react": "^19.0.8",