orga-build 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.org +59 -12
- package/lib/__tests__/build.test.js +62 -1
- package/lib/build.d.ts +1 -1
- package/lib/build.d.ts.map +1 -1
- package/lib/build.js +35 -6
- package/lib/config.d.ts +4 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +6 -2
- package/lib/endpoint.d.ts +23 -0
- package/lib/endpoint.d.ts.map +1 -0
- package/lib/endpoint.js +49 -0
- package/lib/files.d.ts +24 -5
- package/lib/files.d.ts.map +1 -1
- package/lib/files.js +129 -20
- package/lib/plugin.d.ts +7 -2
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +52 -6
- package/lib/serve.js +2 -2
- package/lib/ssr.jsx +2 -0
- package/lib/vite.d.ts +3 -1
- package/lib/vite.d.ts.map +1 -1
- package/lib/vite.js +57 -14
- package/package.json +1 -1
package/README.org
CHANGED
|
@@ -78,29 +78,35 @@ export default {
|
|
|
78
78
|
// CSS class(es) to wrap rendered org content
|
|
79
79
|
containerClass: ['prose', 'prose-lg'],
|
|
80
80
|
|
|
81
|
-
// Global
|
|
82
|
-
//
|
|
83
|
-
styles: ['/style.css'],
|
|
81
|
+
// Global stylesheets — paths relative to orga.config.js (leading / optional).
|
|
82
|
+
// Injected in dev SSR <head> and imported by the client entry.
|
|
83
|
+
styles: ['pages/style.css'],
|
|
84
84
|
|
|
85
85
|
// Extra rehype plugins appended to orga-build defaults
|
|
86
86
|
// Useful for syntax highlighting (e.g. rehype-pretty-code).
|
|
87
87
|
rehypePlugins: [],
|
|
88
88
|
|
|
89
89
|
// Additional Vite plugins
|
|
90
|
-
vitePlugins: []
|
|
90
|
+
vitePlugins: [],
|
|
91
|
+
|
|
92
|
+
// Glob patterns (relative to root) to exclude from content scanning.
|
|
93
|
+
// Useful for generated or declaration files that must live inside root
|
|
94
|
+
// but should not be treated as pages or endpoints.
|
|
95
|
+
exclude: ['**/*.d.ts']
|
|
91
96
|
}
|
|
92
97
|
#+end_src
|
|
93
98
|
|
|
94
99
|
** Configuration Options
|
|
95
100
|
|
|
96
|
-
| Option
|
|
97
|
-
|
|
98
|
-
| =root=
|
|
99
|
-
| =outDir=
|
|
100
|
-
| =containerClass= | =string \vert string[]= | =[]=
|
|
101
|
-
| =styles=
|
|
102
|
-
| =
|
|
103
|
-
| =
|
|
101
|
+
| Option | Type | Default | Description |
|
|
102
|
+
|----------------+-------------------+---------+-----------------------------------------------------------------|
|
|
103
|
+
| =root= | =string= | ='pages'= | Directory containing content files |
|
|
104
|
+
| =outDir= | =string= | ='out'= | Output directory for production build |
|
|
105
|
+
| =containerClass= | =string \vert string[]= | =[]= | CSS class(es) for content wrapper |
|
|
106
|
+
| =styles= | =string[]= | =[]= | Stylesheets to inject/import; paths relative to =orga.config.js= |
|
|
107
|
+
| =exclude= | =string[]= | =[]= | Glob patterns (relative to =root=) excluded from content scanning |
|
|
108
|
+
| =rehypePlugins= | =PluggableList= | =[]= | Extra rehype plugins appended to orga-build defaults |
|
|
109
|
+
| =vitePlugins= | =PluginOption[]= | =[]= | Additional Vite plugins |
|
|
104
110
|
|
|
105
111
|
** Syntax Highlighting Example
|
|
106
112
|
|
|
@@ -112,6 +118,47 @@ export default {
|
|
|
112
118
|
}
|
|
113
119
|
#+end_src
|
|
114
120
|
|
|
121
|
+
* Routing
|
|
122
|
+
|
|
123
|
+
orga-build supports two route types: *page routes* and *endpoint routes*.
|
|
124
|
+
|
|
125
|
+
** Page Routes
|
|
126
|
+
|
|
127
|
+
Page routes are discovered from =.org=, =.tsx=, and =.jsx= files.
|
|
128
|
+
|
|
129
|
+
- =index.org= -> =/=
|
|
130
|
+
- =about.org= -> =/about=
|
|
131
|
+
- =docs/getting-started.tsx= -> =/docs/getting-started=
|
|
132
|
+
|
|
133
|
+
At build time, page routes are emitted as HTML:
|
|
134
|
+
|
|
135
|
+
- =/about= -> =out/about/index.html=
|
|
136
|
+
|
|
137
|
+
** Endpoint Routes
|
|
138
|
+
|
|
139
|
+
Endpoint routes are discovered from =.ts=, =.js=, =.mts=, and =.mjs= files where the basename already includes a target extension (for example =rss.xml.ts= or =data.json.ts=).
|
|
140
|
+
|
|
141
|
+
- =rss.xml.ts= -> =/rss.xml=
|
|
142
|
+
- =nested/feed.xml.ts= -> =/nested/feed.xml=
|
|
143
|
+
- =api/data.json.ts= -> =/api/data.json=
|
|
144
|
+
|
|
145
|
+
Endpoint modules must export:
|
|
146
|
+
|
|
147
|
+
#+begin_src ts
|
|
148
|
+
export async function GET(ctx) {
|
|
149
|
+
return new Response('ok', {
|
|
150
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' }
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
#+end_src
|
|
154
|
+
|
|
155
|
+
At build time, endpoint routes are emitted to exact filenames:
|
|
156
|
+
|
|
157
|
+
- =/rss.xml= -> =out/rss.xml=
|
|
158
|
+
- =/api/data.json= -> =out/api/data.json=
|
|
159
|
+
|
|
160
|
+
Route conflicts (same final route path) fail fast during dev/build startup.
|
|
161
|
+
|
|
115
162
|
* TypeScript Setup
|
|
116
163
|
|
|
117
164
|
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.
|
|
@@ -49,6 +49,19 @@ Here's [[mailto:hi@unclex.net][send me an email]].
|
|
|
49
49
|
'Docs index page.'
|
|
50
50
|
)
|
|
51
51
|
await fs.writeFile(path.join(fixtureDir, 'more.org'), 'Another page.')
|
|
52
|
+
await fs.writeFile(
|
|
53
|
+
path.join(fixtureDir, 'rss.xml.ts'),
|
|
54
|
+
`import { getPages } from 'orga-build:content'
|
|
55
|
+
|
|
56
|
+
export function GET() {
|
|
57
|
+
const pages = getPages()
|
|
58
|
+
return new Response(
|
|
59
|
+
'<?xml version="1.0" encoding="UTF-8"?><rss><count>' + pages.length + '</count></rss>',
|
|
60
|
+
{ headers: { 'content-type': 'application/xml; charset=utf-8' } }
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
)
|
|
52
65
|
await fs.writeFile(
|
|
53
66
|
path.join(fixtureDir, 'style.css'),
|
|
54
67
|
'.global-style-marker { color: rgb(1, 2, 3); }'
|
|
@@ -103,11 +116,13 @@ Here's [[mailto:hi@unclex.net][send me an email]].
|
|
|
103
116
|
})
|
|
104
117
|
|
|
105
118
|
test('processes configured global styles through vite and injects built css', async () => {
|
|
119
|
+
const styleUrl =
|
|
120
|
+
'/' + path.relative(process.cwd(), path.join(fixtureDir, 'style.css'))
|
|
106
121
|
await build({
|
|
107
122
|
root: fixtureDir,
|
|
108
123
|
outDir: outDir,
|
|
109
124
|
containerClass: [],
|
|
110
|
-
styles: [
|
|
125
|
+
styles: [styleUrl],
|
|
111
126
|
vitePlugins: [],
|
|
112
127
|
preBuild: [],
|
|
113
128
|
postBuild: []
|
|
@@ -169,4 +184,50 @@ This page verifies custom rehype plugins.`
|
|
|
169
184
|
await fs.rm(fixtureDirRehype, { recursive: true, force: true })
|
|
170
185
|
}
|
|
171
186
|
})
|
|
187
|
+
|
|
188
|
+
test('emits endpoint routes with exact output filenames', async () => {
|
|
189
|
+
await build({
|
|
190
|
+
root: fixtureDir,
|
|
191
|
+
outDir: outDir,
|
|
192
|
+
containerClass: [],
|
|
193
|
+
vitePlugins: [],
|
|
194
|
+
preBuild: [],
|
|
195
|
+
postBuild: []
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const rss = await fs.readFile(path.join(outDir, 'rss.xml'), 'utf-8')
|
|
199
|
+
assert.ok(
|
|
200
|
+
rss.includes('<rss>') && rss.includes('<count>'),
|
|
201
|
+
'should emit rss.xml from GET endpoint'
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('fails on duplicate route conflicts', async () => {
|
|
206
|
+
const fixtureDirConflict = path.join(__dirname, 'fixtures-conflict')
|
|
207
|
+
const outDirConflict = path.join(__dirname, '.test-output-conflict')
|
|
208
|
+
try {
|
|
209
|
+
await fs.mkdir(fixtureDirConflict, { recursive: true })
|
|
210
|
+
await fs.writeFile(path.join(fixtureDirConflict, 'index.org'), 'Home')
|
|
211
|
+
await fs.writeFile(
|
|
212
|
+
path.join(fixtureDirConflict, 'index.tsx'),
|
|
213
|
+
'export default function Page() { return <div>Index</div> }'
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
await assert.rejects(
|
|
217
|
+
() =>
|
|
218
|
+
build({
|
|
219
|
+
root: fixtureDirConflict,
|
|
220
|
+
outDir: outDirConflict,
|
|
221
|
+
containerClass: [],
|
|
222
|
+
vitePlugins: [],
|
|
223
|
+
preBuild: [],
|
|
224
|
+
postBuild: []
|
|
225
|
+
}),
|
|
226
|
+
/Route conflict detected/
|
|
227
|
+
)
|
|
228
|
+
} finally {
|
|
229
|
+
await fs.rm(outDirConflict, { recursive: true, force: true })
|
|
230
|
+
await fs.rm(fixtureDirConflict, { recursive: true, force: true })
|
|
231
|
+
}
|
|
232
|
+
})
|
|
172
233
|
})
|
package/lib/build.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @param {import('./config.js').Config} config
|
|
3
3
|
* @param {string} [projectRoot]
|
|
4
4
|
*/
|
|
5
|
-
export function build({ outDir, root, containerClass, styles, rehypePlugins, vitePlugins }: import("./config.js").Config, projectRoot?: string): Promise<void>;
|
|
5
|
+
export function build({ outDir, root, containerClass, styles, rehypePlugins, vitePlugins, exclude }: import("./config.js").Config, projectRoot?: string): Promise<void>;
|
|
6
6
|
export { alias };
|
|
7
7
|
import { alias } from './plugin.js';
|
|
8
8
|
//# sourceMappingURL=build.d.ts.map
|
package/lib/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["build.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["build.js"],"names":[],"mappings":"AAgBA;;;GAGG;AACH,qGAHW,OAAO,aAAa,EAAE,MAAM,gBAC5B,MAAM,iBA0LhB;;sBAtM4C,aAAa"}
|
package/lib/build.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
4
|
import { createBuilder } from 'vite'
|
|
5
|
+
import { resolveEndpointResponse } from './endpoint.js'
|
|
5
6
|
import { emptyDir, ensureDir, exists } from './fs.js'
|
|
6
7
|
import { alias, createOrgaBuildConfig } from './plugin.js'
|
|
7
8
|
import { escapeHtml } from './util.js'
|
|
@@ -24,7 +25,8 @@ export async function build(
|
|
|
24
25
|
containerClass,
|
|
25
26
|
styles = [],
|
|
26
27
|
rehypePlugins = [],
|
|
27
|
-
vitePlugins = []
|
|
28
|
+
vitePlugins = [],
|
|
29
|
+
exclude = []
|
|
28
30
|
},
|
|
29
31
|
projectRoot = process.cwd()
|
|
30
32
|
) {
|
|
@@ -38,12 +40,12 @@ export async function build(
|
|
|
38
40
|
containerClass,
|
|
39
41
|
styles,
|
|
40
42
|
rehypePlugins,
|
|
41
|
-
vitePlugins
|
|
43
|
+
vitePlugins,
|
|
44
|
+
exclude
|
|
42
45
|
})
|
|
43
46
|
|
|
44
47
|
// Shared config with environment-specific build settings
|
|
45
48
|
const builder = await createBuilder({
|
|
46
|
-
root,
|
|
47
49
|
plugins,
|
|
48
50
|
resolve,
|
|
49
51
|
ssr: { noExternal: true },
|
|
@@ -83,9 +85,11 @@ export async function build(
|
|
|
83
85
|
console.log('preparing ssr bundle...')
|
|
84
86
|
await builder.build(builder.environments.ssr)
|
|
85
87
|
|
|
86
|
-
const {
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
const {
|
|
89
|
+
render,
|
|
90
|
+
pages,
|
|
91
|
+
endpoints = {}
|
|
92
|
+
} = await import(pathToFileURL(path.join(ssrOutDir, 'ssr.mjs')).toString())
|
|
89
93
|
|
|
90
94
|
// Build client bundle
|
|
91
95
|
const _clientResult = await builder.build(builder.environments.client)
|
|
@@ -129,6 +133,31 @@ export async function build(
|
|
|
129
133
|
})
|
|
130
134
|
)
|
|
131
135
|
|
|
136
|
+
const endpointPaths = Object.keys(endpoints)
|
|
137
|
+
await Promise.all(
|
|
138
|
+
endpointPaths.map(async (route) => {
|
|
139
|
+
const endpointModule = endpoints[route]
|
|
140
|
+
const ctx = {
|
|
141
|
+
url: new URL(`http://localhost${route}`),
|
|
142
|
+
params: {},
|
|
143
|
+
mode: /** @type {'build'} */ ('build'),
|
|
144
|
+
route: { route }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const response = await resolveEndpointResponse(endpointModule, ctx, 'GET')
|
|
148
|
+
if (response.status < 200 || response.status >= 300) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Endpoint route "${route}" returned non-2xx status during build: ${response.status}`
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const bytes = Buffer.from(await response.arrayBuffer())
|
|
155
|
+
const writePath = path.join(clientOutDir, route.replace(/^\//, ''))
|
|
156
|
+
await ensureDir(path.dirname(writePath))
|
|
157
|
+
await fs.writeFile(writePath, bytes)
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
|
|
132
161
|
await fs.rm(ssrOutDir, { recursive: true })
|
|
133
162
|
|
|
134
163
|
return
|
package/lib/config.d.ts
CHANGED
|
@@ -24,5 +24,9 @@ export type Config = {
|
|
|
24
24
|
* - Extra rehype plugins appended to orga-build defaults
|
|
25
25
|
*/
|
|
26
26
|
rehypePlugins?: import("unified").PluggableList;
|
|
27
|
+
/**
|
|
28
|
+
* - Glob patterns for files to exclude from content scanning
|
|
29
|
+
*/
|
|
30
|
+
exclude?: string[];
|
|
27
31
|
};
|
|
28
32
|
//# sourceMappingURL=config.d.ts.map
|
package/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.js"],"names":[],"mappings":"AA6BA;;;GAGG;AACH,qCAHW,MAAM,EAAE,GACN,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD5D;;YA3Ea,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;;;;cAC/B,MAAM,EAAE"}
|
package/lib/config.js
CHANGED
|
@@ -11,6 +11,7 @@ import path from 'node:path'
|
|
|
11
11
|
* @property {string[]|string} containerClass
|
|
12
12
|
* @property {string[]} [styles] - Global stylesheet URLs injected in dev SSR and imported by client entry
|
|
13
13
|
* @property {import('unified').PluggableList} [rehypePlugins] - Extra rehype plugins appended to orga-build defaults
|
|
14
|
+
* @property {string[]} [exclude] - Glob patterns for files to exclude from content scanning
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
/** @type {Config} */
|
|
@@ -22,7 +23,8 @@ const defaultConfig = {
|
|
|
22
23
|
vitePlugins: [],
|
|
23
24
|
containerClass: [],
|
|
24
25
|
styles: [],
|
|
25
|
-
rehypePlugins: []
|
|
26
|
+
rehypePlugins: [],
|
|
27
|
+
exclude: []
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -68,7 +70,9 @@ export async function loadConfig(...files) {
|
|
|
68
70
|
result.outDir = resolveConfigPath(result.outDir)
|
|
69
71
|
const styles = result.styles
|
|
70
72
|
result.styles = Array.isArray(styles)
|
|
71
|
-
? styles
|
|
73
|
+
? styles
|
|
74
|
+
.filter((v) => typeof v === 'string')
|
|
75
|
+
.map((v) => '/' + v.replace(/^\/+/, ''))
|
|
72
76
|
: []
|
|
73
77
|
return {
|
|
74
78
|
config: result,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} EndpointContext
|
|
3
|
+
* @property {URL} url
|
|
4
|
+
* @property {Record<string, string>} params
|
|
5
|
+
* @property {'dev' | 'build'} mode
|
|
6
|
+
* @property {{ route: string }} route
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* @param {Record<string, any>} endpointModule
|
|
10
|
+
* @param {EndpointContext} ctx
|
|
11
|
+
* @param {string} method
|
|
12
|
+
* @returns {Promise<Response>}
|
|
13
|
+
*/
|
|
14
|
+
export function resolveEndpointResponse(endpointModule: Record<string, any>, ctx: EndpointContext, method?: string): Promise<Response>;
|
|
15
|
+
export type EndpointContext = {
|
|
16
|
+
url: URL;
|
|
17
|
+
params: Record<string, string>;
|
|
18
|
+
mode: "dev" | "build";
|
|
19
|
+
route: {
|
|
20
|
+
route: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=endpoint.d.ts.map
|
|
@@ -0,0 +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"}
|
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} EndpointContext
|
|
3
|
+
* @property {URL} url
|
|
4
|
+
* @property {Record<string, string>} params
|
|
5
|
+
* @property {'dev' | 'build'} mode
|
|
6
|
+
* @property {{ route: string }} route
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Record<string, any>} endpointModule
|
|
11
|
+
* @param {EndpointContext} ctx
|
|
12
|
+
* @param {string} method
|
|
13
|
+
* @returns {Promise<Response>}
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveEndpointResponse(
|
|
16
|
+
endpointModule,
|
|
17
|
+
ctx,
|
|
18
|
+
method = 'GET'
|
|
19
|
+
) {
|
|
20
|
+
const route = ctx.route.route
|
|
21
|
+
|
|
22
|
+
if (method === 'HEAD' && typeof endpointModule.HEAD === 'function') {
|
|
23
|
+
const res = await endpointModule.HEAD(ctx)
|
|
24
|
+
if (!(res instanceof Response))
|
|
25
|
+
throw new Error(`Endpoint route "${route}" HEAD must return Response`)
|
|
26
|
+
return res
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof endpointModule.GET !== 'function') {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Endpoint route "${route}" must export GET(ctx) returning Response`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const res = await endpointModule.GET(ctx)
|
|
36
|
+
if (!(res instanceof Response)) {
|
|
37
|
+
throw new Error(`Endpoint route "${route}" GET must return Response`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (method === 'HEAD') {
|
|
41
|
+
return new Response(null, {
|
|
42
|
+
status: res.status,
|
|
43
|
+
statusText: res.statusText,
|
|
44
|
+
headers: new Headers(res.headers)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return res
|
|
49
|
+
}
|
package/lib/files.d.ts
CHANGED
|
@@ -2,15 +2,30 @@
|
|
|
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
|
|
5
6
|
*/
|
|
6
|
-
export function setup(dir: string, { outDir }?: {
|
|
7
|
+
export function setup(dir: string, { outDir, exclude }?: {
|
|
7
8
|
outDir?: string | undefined;
|
|
9
|
+
exclude?: string[] | undefined;
|
|
8
10
|
}): {
|
|
9
|
-
pages: () => Promise<Record<string, Page
|
|
11
|
+
pages: (() => Promise<Record<string, Page>>) & {
|
|
12
|
+
invalidate: () => void;
|
|
13
|
+
};
|
|
10
14
|
page: (slug: string) => Promise<Page>;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
endpoints: (() => Promise<Record<string, EndpointRoute>>) & {
|
|
16
|
+
invalidate: () => void;
|
|
17
|
+
};
|
|
18
|
+
endpoint: (route: string) => Promise<EndpointRoute>;
|
|
19
|
+
components: (() => Promise<string | null>) & {
|
|
20
|
+
invalidate: () => void;
|
|
21
|
+
};
|
|
22
|
+
layouts: (() => Promise<Record<string, string>>) & {
|
|
23
|
+
invalidate: () => void;
|
|
24
|
+
};
|
|
25
|
+
contentEntries: (() => Promise<ContentEntry[]>) & {
|
|
26
|
+
invalidate: () => void;
|
|
27
|
+
};
|
|
28
|
+
invalidate(): void;
|
|
14
29
|
};
|
|
15
30
|
/**
|
|
16
31
|
* Convert a content file path (relative to content root) to the canonical page slug.
|
|
@@ -24,6 +39,10 @@ export type Page = {
|
|
|
24
39
|
*/
|
|
25
40
|
title?: string;
|
|
26
41
|
};
|
|
42
|
+
export type EndpointRoute = {
|
|
43
|
+
route: string;
|
|
44
|
+
dataPath: string;
|
|
45
|
+
};
|
|
27
46
|
export type ContentEntry = {
|
|
28
47
|
id: string;
|
|
29
48
|
slug: string;
|
package/lib/files.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.js"],"names":[],"mappings":"AAoFA;;;;;GAKG;AACH,2BALW,MAAM,wBAEd;IAAyB,MAAM;IACJ,OAAO;CACpC;;0BA6LqD,IAAI;;iBApB7C,MAAM;;0BAoBmC,IAAI;;sBAd7C,MAAM;;0BAcmC,IAAI;;;0BAAJ,IAAI;;;0BAAJ,IAAI;;;EATzD;AA8BD;;;GAGG;AACH,4DAFW,MAAM,UAoBhB;;cA1Ta,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"}
|
package/lib/files.js
CHANGED
|
@@ -10,6 +10,12 @@ import { getSettings } from 'orga'
|
|
|
10
10
|
* Path to the page data file
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} EndpointRoute
|
|
15
|
+
* @property {string} route
|
|
16
|
+
* @property {string} dataPath
|
|
17
|
+
*/
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* @typedef {Object} ContentEntry
|
|
15
21
|
* @property {string} id
|
|
@@ -80,8 +86,9 @@ function getContentId(slug) {
|
|
|
80
86
|
* @param {string} dir
|
|
81
87
|
* @param {object} [options]
|
|
82
88
|
* @param {string} [options.outDir] - Output directory to exclude from file discovery
|
|
89
|
+
* @param {string[]} [options.exclude] - Additional glob patterns to exclude from file discovery
|
|
83
90
|
*/
|
|
84
|
-
export function setup(dir, { outDir } = {}) {
|
|
91
|
+
export function setup(dir, { outDir, exclude = [] } = {}) {
|
|
85
92
|
const outDirRelative = outDir ? path.relative(dir, outDir) : null
|
|
86
93
|
// Only exclude outDir if it's inside the root (not an external path like ../out)
|
|
87
94
|
const outDirExclude =
|
|
@@ -89,30 +96,58 @@ export function setup(dir, { outDir } = {}) {
|
|
|
89
96
|
? `!${outDirRelative}/**`
|
|
90
97
|
: null
|
|
91
98
|
|
|
92
|
-
const
|
|
99
|
+
const discoveredRoutes = cache(async function () {
|
|
93
100
|
const files = await globby(
|
|
94
101
|
[
|
|
95
|
-
'**/*.{org,tsx,jsx}',
|
|
102
|
+
'**/*.{org,tsx,jsx,ts,js,mts,mjs}',
|
|
96
103
|
'!**/_*/**',
|
|
97
104
|
'!**/_*',
|
|
98
105
|
'!**/.*/**',
|
|
99
106
|
'!**/.*',
|
|
100
107
|
'!node_modules/**',
|
|
101
|
-
...(outDirExclude ? [outDirExclude] : [])
|
|
108
|
+
...(outDirExclude ? [outDirExclude] : []),
|
|
109
|
+
...exclude.map((p) => `!${p}`)
|
|
102
110
|
],
|
|
103
111
|
{ cwd: dir }
|
|
104
112
|
)
|
|
105
113
|
|
|
106
114
|
/** @type {Record<string, Page>} */
|
|
107
115
|
const pages = {}
|
|
116
|
+
/** @type {Record<string, EndpointRoute>} */
|
|
117
|
+
const endpoints = {}
|
|
118
|
+
/** @type {Map<string, { sourceType: 'page' | 'endpoint', filePath: string }>} */
|
|
119
|
+
const routeOwners = new Map()
|
|
120
|
+
|
|
108
121
|
for (const file of files) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
const absolutePath = path.join(dir, file)
|
|
123
|
+
const pageSlug = getPageSlugFromFilePath(file)
|
|
124
|
+
const endpointRoute = getEndpointRouteFromFilePath(file)
|
|
125
|
+
|
|
126
|
+
if (pageSlug) {
|
|
127
|
+
assertUniqueRoute(routeOwners, pageSlug, 'page', absolutePath)
|
|
128
|
+
pages[pageSlug] = { dataPath: absolutePath }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (endpointRoute) {
|
|
132
|
+
assertUniqueRoute(routeOwners, endpointRoute, 'endpoint', absolutePath)
|
|
133
|
+
endpoints[endpointRoute] = {
|
|
134
|
+
route: endpointRoute,
|
|
135
|
+
dataPath: absolutePath
|
|
136
|
+
}
|
|
112
137
|
}
|
|
113
138
|
}
|
|
114
139
|
|
|
115
|
-
return pages
|
|
140
|
+
return { pages, endpoints }
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const pages = cache(async function () {
|
|
144
|
+
const routes = await discoveredRoutes()
|
|
145
|
+
return routes.pages
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const endpoints = cache(async function () {
|
|
149
|
+
const routes = await discoveredRoutes()
|
|
150
|
+
return routes.endpoints
|
|
116
151
|
})
|
|
117
152
|
|
|
118
153
|
const layouts = cache(async function () {
|
|
@@ -122,7 +157,8 @@ export function setup(dir, { outDir } = {}) {
|
|
|
122
157
|
'!**/.*/**',
|
|
123
158
|
'!**/.*',
|
|
124
159
|
'!node_modules/**',
|
|
125
|
-
...(outDirExclude ? [outDirExclude] : [])
|
|
160
|
+
...(outDirExclude ? [outDirExclude] : []),
|
|
161
|
+
...exclude.map((p) => `!${p}`)
|
|
126
162
|
],
|
|
127
163
|
{
|
|
128
164
|
cwd: dir
|
|
@@ -146,7 +182,8 @@ export function setup(dir, { outDir } = {}) {
|
|
|
146
182
|
'!**/.*/**',
|
|
147
183
|
'!**/.*',
|
|
148
184
|
'!node_modules/**',
|
|
149
|
-
...(outDirExclude ? [outDirExclude] : [])
|
|
185
|
+
...(outDirExclude ? [outDirExclude] : []),
|
|
186
|
+
...exclude.map((p) => `!${p}`)
|
|
150
187
|
],
|
|
151
188
|
{
|
|
152
189
|
cwd: dir
|
|
@@ -202,9 +239,19 @@ export function setup(dir, { outDir } = {}) {
|
|
|
202
239
|
const files = {
|
|
203
240
|
pages,
|
|
204
241
|
page,
|
|
242
|
+
endpoints,
|
|
243
|
+
endpoint,
|
|
205
244
|
components,
|
|
206
245
|
layouts,
|
|
207
|
-
contentEntries
|
|
246
|
+
contentEntries,
|
|
247
|
+
invalidate() {
|
|
248
|
+
discoveredRoutes.invalidate()
|
|
249
|
+
pages.invalidate()
|
|
250
|
+
endpoints.invalidate()
|
|
251
|
+
layouts.invalidate()
|
|
252
|
+
components.invalidate()
|
|
253
|
+
contentEntries.invalidate()
|
|
254
|
+
}
|
|
208
255
|
}
|
|
209
256
|
|
|
210
257
|
return files
|
|
@@ -214,26 +261,40 @@ export function setup(dir, { outDir } = {}) {
|
|
|
214
261
|
const all = await pages()
|
|
215
262
|
return all[slug]
|
|
216
263
|
}
|
|
264
|
+
|
|
265
|
+
/** @param {string} route */
|
|
266
|
+
async function endpoint(route) {
|
|
267
|
+
const all = await endpoints()
|
|
268
|
+
return all[route]
|
|
269
|
+
}
|
|
217
270
|
}
|
|
218
271
|
|
|
219
272
|
/**
|
|
220
273
|
* Creates a cached version of an async function that will only execute once
|
|
221
|
-
* and return the cached result on subsequent calls
|
|
274
|
+
* and return the cached result on subsequent calls. The returned function
|
|
275
|
+
* also has an `invalidate()` method to clear the cache.
|
|
222
276
|
*
|
|
223
277
|
* @template T
|
|
224
278
|
* @param {() => Promise<T>} fn - The async function to cache
|
|
225
|
-
* @returns {() => Promise<T>
|
|
279
|
+
* @returns {(() => Promise<T>) & { invalidate: () => void }}
|
|
226
280
|
*/
|
|
227
281
|
function cache(fn) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
282
|
+
let settled = false
|
|
283
|
+
/** @type {T | undefined} */
|
|
284
|
+
let value
|
|
285
|
+
/** @returns {Promise<T>} */
|
|
286
|
+
async function cached() {
|
|
287
|
+
if (!settled) {
|
|
288
|
+
value = await fn()
|
|
289
|
+
settled = true
|
|
233
290
|
}
|
|
234
|
-
|
|
235
|
-
|
|
291
|
+
return /** @type {T} */ (value)
|
|
292
|
+
}
|
|
293
|
+
cached.invalidate = function () {
|
|
294
|
+
settled = false
|
|
295
|
+
value = undefined
|
|
236
296
|
}
|
|
297
|
+
return cached
|
|
237
298
|
}
|
|
238
299
|
|
|
239
300
|
/**
|
|
@@ -259,3 +320,51 @@ export function getSlugFromContentFilePath(contentFilePath) {
|
|
|
259
320
|
|
|
260
321
|
return slug
|
|
261
322
|
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {string} filePath
|
|
326
|
+
* @returns {string|null}
|
|
327
|
+
*/
|
|
328
|
+
function getPageSlugFromFilePath(filePath) {
|
|
329
|
+
if (!/\.(org|tsx|jsx)$/.test(filePath)) {
|
|
330
|
+
return null
|
|
331
|
+
}
|
|
332
|
+
return getSlugFromContentFilePath(filePath)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* @param {string} filePath
|
|
337
|
+
* @returns {string|null}
|
|
338
|
+
*/
|
|
339
|
+
function getEndpointRouteFromFilePath(filePath) {
|
|
340
|
+
if (!/\.(ts|js|mts|mjs)$/.test(filePath)) {
|
|
341
|
+
return null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const normalizedFilePath = filePath.replace(/\\/g, '/')
|
|
345
|
+
const targetPath = normalizedFilePath.replace(/\.(ts|js|mts|mjs)$/, '')
|
|
346
|
+
const basename = path.posix.basename(targetPath)
|
|
347
|
+
|
|
348
|
+
// Endpoint files must carry a target extension: rss.xml.ts, data.json.ts, etc.
|
|
349
|
+
if (!basename.includes('.')) {
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return `/${targetPath.replace(/^\/+/, '')}`
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @param {Map<string, { sourceType: 'page' | 'endpoint', filePath: string }>} routeOwners
|
|
358
|
+
* @param {string} route
|
|
359
|
+
* @param {'page' | 'endpoint'} sourceType
|
|
360
|
+
* @param {string} filePath
|
|
361
|
+
*/
|
|
362
|
+
function assertUniqueRoute(routeOwners, route, sourceType, filePath) {
|
|
363
|
+
const existing = routeOwners.get(route)
|
|
364
|
+
if (existing) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Route conflict detected for "${route}"\n- ${existing.sourceType}: ${existing.filePath}\n- ${sourceType}: ${filePath}`
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
routeOwners.set(route, { sourceType, filePath })
|
|
370
|
+
}
|
package/lib/plugin.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @property {string|string[]} [containerClass] - CSS class(es) to wrap rendered content
|
|
6
6
|
* @property {string[]} [styles] - Global stylesheet URLs to import/inject
|
|
7
7
|
* @property {import('unified').PluggableList} [rehypePlugins] - Extra rehype plugins appended to orga-build defaults
|
|
8
|
+
* @property {string[]} [exclude] - Glob patterns for files to exclude from content scanning
|
|
8
9
|
*/
|
|
9
10
|
/**
|
|
10
11
|
* Creates the canonical orga-build plugin preset.
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
* @param {OrgaBuildPluginOptions} options
|
|
14
15
|
* @returns {import('vite').PluginOption[]}
|
|
15
16
|
*/
|
|
16
|
-
export function orgaBuildPlugin({ root, outDir, containerClass, styles, rehypePlugins }: OrgaBuildPluginOptions): import("vite").PluginOption[];
|
|
17
|
+
export function orgaBuildPlugin({ root, outDir, containerClass, styles, rehypePlugins, exclude }: OrgaBuildPluginOptions): import("vite").PluginOption[];
|
|
17
18
|
/**
|
|
18
19
|
* Creates the full Vite config options for orga-build.
|
|
19
20
|
* Includes plugins, resolve aliases, and other shared config.
|
|
@@ -21,7 +22,7 @@ export function orgaBuildPlugin({ root, outDir, containerClass, styles, rehypePl
|
|
|
21
22
|
* @param {OrgaBuildPluginOptions & { outDir?: string, vitePlugins?: import('vite').PluginOption[], includeFallbackHtml?: boolean, projectRoot?: string }} options
|
|
22
23
|
* @returns {{ plugins: import('vite').PluginOption[], resolve: { alias: typeof alias } }}
|
|
23
24
|
*/
|
|
24
|
-
export function createOrgaBuildConfig({ root, outDir, containerClass, styles, rehypePlugins, vitePlugins, includeFallbackHtml, projectRoot }: OrgaBuildPluginOptions & {
|
|
25
|
+
export function createOrgaBuildConfig({ root, outDir, containerClass, styles, rehypePlugins, vitePlugins, includeFallbackHtml, projectRoot, exclude }: OrgaBuildPluginOptions & {
|
|
25
26
|
outDir?: string;
|
|
26
27
|
vitePlugins?: import("vite").PluginOption[];
|
|
27
28
|
includeFallbackHtml?: boolean;
|
|
@@ -76,5 +77,9 @@ export type OrgaBuildPluginOptions = {
|
|
|
76
77
|
* - Extra rehype plugins appended to orga-build defaults
|
|
77
78
|
*/
|
|
78
79
|
rehypePlugins?: import("unified").PluggableList;
|
|
80
|
+
/**
|
|
81
|
+
* - Glob patterns for files to exclude from content scanning
|
|
82
|
+
*/
|
|
83
|
+
exclude?: string[];
|
|
79
84
|
};
|
|
80
85
|
//# sourceMappingURL=plugin.d.ts.map
|
package/lib/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["plugin.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["plugin.js"],"names":[],"mappings":"AAwBA;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,kGAHW,sBAAsB,GACpB,OAAO,MAAM,EAAE,YAAY,EAAE,CAezC;AAED;;;;;;GAMG;AACH,uJAHW,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,CAiCxF;AAiBD;;;;;;;;;;;;;GAaG;AACH,gDAJW,MAAM,WACN,MAAM,EAAE,GACN,OAAO,MAAM,EAAE,MAAM,CAsHjC;AAlOD;;GAEG;AACH;;;;EAIC;;;;;UAIa,MAAM;;;;aACN,MAAM,GAAG,SAAS;;;;qBAClB,MAAM,GAAC,MAAM,EAAE;;;;aACf,MAAM,EAAE;;;;oBACR,OAAO,SAAS,EAAE,aAAa;;;;cAC/B,MAAM,EAAE"}
|
package/lib/plugin.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import react from '@vitejs/plugin-react'
|
|
6
6
|
import { createServerModuleRunner } from 'vite'
|
|
7
|
+
import { resolveEndpointResponse } from './endpoint.js'
|
|
7
8
|
import { setupOrga } from './orga.js'
|
|
8
9
|
import { escapeHtml } from './util.js'
|
|
9
10
|
import { pluginFactory } from './vite.js'
|
|
@@ -28,6 +29,7 @@ export const alias = {
|
|
|
28
29
|
* @property {string|string[]} [containerClass] - CSS class(es) to wrap rendered content
|
|
29
30
|
* @property {string[]} [styles] - Global stylesheet URLs to import/inject
|
|
30
31
|
* @property {import('unified').PluggableList} [rehypePlugins] - Extra rehype plugins appended to orga-build defaults
|
|
32
|
+
* @property {string[]} [exclude] - Glob patterns for files to exclude from content scanning
|
|
31
33
|
*/
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -42,12 +44,13 @@ export function orgaBuildPlugin({
|
|
|
42
44
|
outDir,
|
|
43
45
|
containerClass = [],
|
|
44
46
|
styles = [],
|
|
45
|
-
rehypePlugins = []
|
|
47
|
+
rehypePlugins = [],
|
|
48
|
+
exclude = []
|
|
46
49
|
}) {
|
|
47
50
|
return [
|
|
48
51
|
setupOrga({ containerClass, root, rehypePlugins }),
|
|
49
52
|
react(),
|
|
50
|
-
pluginFactory({ dir: root, outDir, styles })
|
|
53
|
+
pluginFactory({ dir: root, outDir, styles, exclude })
|
|
51
54
|
]
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -66,11 +69,19 @@ export function createOrgaBuildConfig({
|
|
|
66
69
|
rehypePlugins = [],
|
|
67
70
|
vitePlugins = [],
|
|
68
71
|
includeFallbackHtml = false,
|
|
69
|
-
projectRoot = process.cwd()
|
|
72
|
+
projectRoot = process.cwd(),
|
|
73
|
+
exclude = []
|
|
70
74
|
}) {
|
|
71
75
|
const plugins = [
|
|
72
76
|
...vitePlugins,
|
|
73
|
-
...orgaBuildPlugin({
|
|
77
|
+
...orgaBuildPlugin({
|
|
78
|
+
root,
|
|
79
|
+
outDir,
|
|
80
|
+
containerClass,
|
|
81
|
+
styles,
|
|
82
|
+
rehypePlugins,
|
|
83
|
+
exclude
|
|
84
|
+
})
|
|
74
85
|
]
|
|
75
86
|
if (includeFallbackHtml) {
|
|
76
87
|
// HTML fallback must be first so it can handle HTML navigation requests
|
|
@@ -134,6 +145,43 @@ export function htmlFallbackPlugin(projectRoot, styles = []) {
|
|
|
134
145
|
return next()
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
const url = req.url || '/'
|
|
149
|
+
const pathname = url.split('?')[0]
|
|
150
|
+
|
|
151
|
+
// Endpoint routes are handled first and bypass HTML fallback.
|
|
152
|
+
try {
|
|
153
|
+
const { endpoints } = await runner.import(ssrEntry)
|
|
154
|
+
const endpointModule = endpoints?.[pathname]
|
|
155
|
+
if (endpointModule) {
|
|
156
|
+
const ctx = {
|
|
157
|
+
url: new URL(url, `http://${req.headers.host || 'localhost'}`),
|
|
158
|
+
params: {},
|
|
159
|
+
mode: /** @type {'dev'} */ ('dev'),
|
|
160
|
+
route: { route: pathname }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const response = await resolveEndpointResponse(
|
|
164
|
+
endpointModule,
|
|
165
|
+
ctx,
|
|
166
|
+
req.method
|
|
167
|
+
)
|
|
168
|
+
res.statusCode = response.status
|
|
169
|
+
response.headers.forEach((headerValue, headerName) => {
|
|
170
|
+
res.setHeader(headerName, headerValue)
|
|
171
|
+
})
|
|
172
|
+
if (req.method === 'HEAD') {
|
|
173
|
+
res.end()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
const bytes = Buffer.from(await response.arrayBuffer())
|
|
177
|
+
res.end(bytes)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
next(e)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
137
185
|
// Only handle browser-like navigation requests.
|
|
138
186
|
// Don't match generic */* accepts to avoid hijacking API requests.
|
|
139
187
|
const accept = req.headers.accept || ''
|
|
@@ -142,8 +190,6 @@ export function htmlFallbackPlugin(projectRoot, styles = []) {
|
|
|
142
190
|
}
|
|
143
191
|
|
|
144
192
|
// Don't intercept asset requests (files with extensions)
|
|
145
|
-
const url = req.url || '/'
|
|
146
|
-
const pathname = url.split('?')[0]
|
|
147
193
|
if (pathname !== '/' && /\.\w+$/.test(pathname)) {
|
|
148
194
|
return next()
|
|
149
195
|
}
|
package/lib/serve.js
CHANGED
|
@@ -18,11 +18,11 @@ export async function serve(config, port = 3000, projectRoot = process.cwd()) {
|
|
|
18
18
|
rehypePlugins: config.rehypePlugins ?? [],
|
|
19
19
|
vitePlugins: config.vitePlugins,
|
|
20
20
|
includeFallbackHtml: true,
|
|
21
|
-
projectRoot
|
|
21
|
+
projectRoot,
|
|
22
|
+
exclude: config.exclude ?? []
|
|
22
23
|
})
|
|
23
24
|
|
|
24
25
|
const server = await createServer({
|
|
25
|
-
root: config.root,
|
|
26
26
|
plugins,
|
|
27
27
|
appType: 'custom',
|
|
28
28
|
// Aliases are scoped to the client environment only.
|
package/lib/ssr.jsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { renderToString } from 'react-dom/server'
|
|
2
2
|
import { Router } from 'wouter'
|
|
3
|
+
import endpoints from '/@orga-build/endpoints'
|
|
3
4
|
import pages from '/@orga-build/pages'
|
|
4
5
|
import { App } from './app.jsx'
|
|
5
6
|
|
|
6
7
|
export { pages }
|
|
8
|
+
export { endpoints }
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* @param {string} url
|
package/lib/vite.d.ts
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* @param {string} options.dir
|
|
4
4
|
* @param {string} [options.outDir]
|
|
5
5
|
* @param {string[]} [options.styles]
|
|
6
|
+
* @param {string[]} [options.exclude]
|
|
6
7
|
* @returns {import('vite').Plugin}
|
|
7
8
|
*/
|
|
8
|
-
export function pluginFactory({ dir, outDir, styles }: {
|
|
9
|
+
export function pluginFactory({ dir, outDir, styles, exclude }: {
|
|
9
10
|
dir: string;
|
|
10
11
|
outDir?: string | undefined;
|
|
11
12
|
styles?: string[] | undefined;
|
|
13
|
+
exclude?: string[] | undefined;
|
|
12
14
|
}): import("vite").Plugin;
|
|
13
15
|
export const appEntryId: "/@orga-build/main.js";
|
|
14
16
|
//# 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":"
|
|
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"}
|
package/lib/vite.js
CHANGED
|
@@ -3,19 +3,22 @@ import { setup } from './files.js'
|
|
|
3
3
|
|
|
4
4
|
const magicModulePrefix = '/@orga-build/'
|
|
5
5
|
const pagesModuleId = `${magicModulePrefix}pages`
|
|
6
|
+
const endpointsModuleId = `${magicModulePrefix}endpoints`
|
|
6
7
|
export const appEntryId = `${magicModulePrefix}main.js`
|
|
7
8
|
const contentModuleId = 'orga-build:content'
|
|
8
9
|
const contentModuleIdResolved = `\0${contentModuleId}`
|
|
10
|
+
const endpointModulePrefix = `${endpointsModuleId}/__route__/`
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* @param {Object} options
|
|
12
14
|
* @param {string} options.dir
|
|
13
15
|
* @param {string} [options.outDir]
|
|
14
16
|
* @param {string[]} [options.styles]
|
|
17
|
+
* @param {string[]} [options.exclude]
|
|
15
18
|
* @returns {import('vite').Plugin}
|
|
16
19
|
*/
|
|
17
|
-
export function pluginFactory({ dir, outDir, styles = [] }) {
|
|
18
|
-
const files = setup(dir, { outDir })
|
|
20
|
+
export function pluginFactory({ dir, outDir, styles = [], exclude = [] }) {
|
|
21
|
+
const files = setup(dir, { outDir, exclude })
|
|
19
22
|
|
|
20
23
|
return {
|
|
21
24
|
name: 'vite-plugin-orga-pages',
|
|
@@ -28,7 +31,15 @@ export function pluginFactory({ dir, outDir, styles = [] }) {
|
|
|
28
31
|
}
|
|
29
32
|
}),
|
|
30
33
|
|
|
34
|
+
async configureServer(_server) {
|
|
35
|
+
// Eagerly run file discovery so route conflicts surface at startup
|
|
36
|
+
await files.pages()
|
|
37
|
+
await files.endpoints()
|
|
38
|
+
},
|
|
39
|
+
|
|
31
40
|
hotUpdate() {
|
|
41
|
+
// Invalidate in-memory file caches so added/removed routes are picked up
|
|
42
|
+
files.invalidate()
|
|
32
43
|
// Invalidate content module when content files change
|
|
33
44
|
const module = this.environment.moduleGraph.getModuleById(
|
|
34
45
|
contentModuleIdResolved
|
|
@@ -64,6 +75,9 @@ export function pluginFactory({ dir, outDir, styles = [] }) {
|
|
|
64
75
|
if (id === pagesModuleId) {
|
|
65
76
|
return await renderPageList()
|
|
66
77
|
}
|
|
78
|
+
if (id === endpointsModuleId) {
|
|
79
|
+
return await renderEndpointList()
|
|
80
|
+
}
|
|
67
81
|
if (id.startsWith(pagesModuleId)) {
|
|
68
82
|
const pageId = id.replace(pagesModuleId, '')
|
|
69
83
|
const page = await files.page(pageId)
|
|
@@ -74,6 +88,14 @@ export {default} from '${page.dataPath}';
|
|
|
74
88
|
`
|
|
75
89
|
}
|
|
76
90
|
}
|
|
91
|
+
if (id.startsWith(endpointModulePrefix)) {
|
|
92
|
+
const routeHex = id.slice(endpointModulePrefix.length)
|
|
93
|
+
const endpointId = Buffer.from(routeHex, 'hex').toString('utf-8')
|
|
94
|
+
const endpoint = await files.endpoint(endpointId)
|
|
95
|
+
if (endpoint) {
|
|
96
|
+
return `export * from '${endpoint.dataPath}';`
|
|
97
|
+
}
|
|
98
|
+
}
|
|
77
99
|
|
|
78
100
|
if (id === `${magicModulePrefix}layouts`) {
|
|
79
101
|
const layouts = await files.layouts()
|
|
@@ -99,19 +121,40 @@ export default layouts;
|
|
|
99
121
|
|
|
100
122
|
async function renderPageList() {
|
|
101
123
|
const pages = await files.pages()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
return renderModuleMap('pages', pages, (id) =>
|
|
125
|
+
path.join(magicModulePrefix, 'pages', id)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function renderEndpointList() {
|
|
130
|
+
const endpoints = await files.endpoints()
|
|
131
|
+
return renderModuleMap(
|
|
132
|
+
'endpoints',
|
|
133
|
+
endpoints,
|
|
134
|
+
(route) => endpointModulePrefix + Buffer.from(route).toString('hex')
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string} name
|
|
140
|
+
* @param {Record<string, unknown>} entries
|
|
141
|
+
* @param {(key: string) => string} toModulePath
|
|
142
|
+
*/
|
|
143
|
+
function renderModuleMap(name, entries, toModulePath) {
|
|
144
|
+
/** @type {string[]} */
|
|
145
|
+
const imports = []
|
|
146
|
+
/** @type {string[]} */
|
|
147
|
+
const assignments = []
|
|
148
|
+
Object.keys(entries).forEach((key, i) => {
|
|
149
|
+
imports.push(`import * as m${i} from '${toModulePath(key)}'`)
|
|
150
|
+
assignments.push(`${name}['${key}'] = m${i}`)
|
|
108
151
|
})
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
export default
|
|
114
|
-
|
|
152
|
+
return [
|
|
153
|
+
imports.join('\n'),
|
|
154
|
+
`const ${name} = {};`,
|
|
155
|
+
assignments.join('\n'),
|
|
156
|
+
`export default ${name};`
|
|
157
|
+
].join('\n')
|
|
115
158
|
}
|
|
116
159
|
|
|
117
160
|
async function renderComponents() {
|