orga-build 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.org +282 -33
- package/cli.js +4 -4
- package/lib/__tests__/build.test.js +441 -22
- package/lib/__tests__/dev.test.d.ts +2 -0
- package/lib/__tests__/dev.test.d.ts.map +1 -0
- package/lib/__tests__/dev.test.js +295 -0
- package/lib/__tests__/fixtures.d.ts +8 -0
- package/lib/__tests__/fixtures.d.ts.map +1 -0
- package/lib/__tests__/fixtures.js +61 -0
- package/lib/app.jsx +23 -50
- package/lib/build.d.ts +4 -1
- package/lib/build.d.ts.map +1 -1
- package/lib/build.js +10 -175
- package/lib/config.d.ts +5 -3
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +8 -16
- package/lib/content.d.ts +6 -0
- package/lib/dev-ssr.d.ts +14 -0
- package/lib/dev-ssr.d.ts.map +1 -0
- package/lib/dev-ssr.js +137 -0
- package/lib/endpoint.d.ts +5 -0
- package/lib/endpoint.d.ts.map +1 -1
- package/lib/endpoint.js +1 -0
- package/lib/files.d.ts +8 -3
- package/lib/files.d.ts.map +1 -1
- package/lib/files.js +104 -20
- package/lib/fs.d.ts +0 -5
- package/lib/fs.d.ts.map +1 -1
- package/lib/fs.js +0 -18
- package/lib/html.d.ts +37 -0
- package/lib/html.d.ts.map +1 -0
- package/lib/html.js +101 -0
- package/lib/index.html +0 -1
- package/lib/island-client.d.ts +5 -0
- package/lib/island-client.d.ts.map +1 -0
- package/lib/island-client.js +41 -0
- package/lib/island.d.ts +18 -0
- package/lib/island.d.ts.map +1 -0
- package/lib/island.js +146 -0
- package/lib/island.jsx +108 -0
- package/lib/orga.d.ts +1 -1
- package/lib/orga.d.ts.map +1 -1
- package/lib/orga.js +54 -16
- package/lib/plugin.d.ts +20 -32
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +128 -178
- package/lib/prerender.d.ts +11 -0
- package/lib/prerender.d.ts.map +1 -0
- package/lib/prerender.js +184 -0
- package/lib/serve.d.ts.map +1 -1
- package/lib/serve.js +5 -23
- package/lib/ssr.jsx +9 -14
- package/lib/util.d.ts +0 -22
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +0 -62
- package/lib/vite.d.ts +5 -6
- package/lib/vite.d.ts.map +1 -1
- package/lib/vite.js +74 -23
- package/package.json +5 -8
- package/lib/components.d.ts +0 -2
- package/lib/components.d.ts.map +0 -1
- package/lib/components.js +0 -1
- package/lib/csr.jsx +0 -11
- package/lib/watch.d.ts +0 -10
- package/lib/watch.d.ts.map +0 -1
- package/lib/watch.js +0 -53
package/lib/plugin.js
CHANGED
|
@@ -1,40 +1,45 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
1
|
import { createRequire } from 'node:module'
|
|
3
2
|
import path from 'node:path'
|
|
4
3
|
import { fileURLToPath } from 'node:url'
|
|
5
4
|
import react from '@vitejs/plugin-react'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { devSsrPlugin } from './dev-ssr.js'
|
|
6
|
+
import { htmlShellPlugin } from './html.js'
|
|
7
|
+
import { islandPlugin } from './island.js'
|
|
8
8
|
import { setupOrga } from './orga.js'
|
|
9
|
-
import {
|
|
9
|
+
import { prerenderPlugin } from './prerender.js'
|
|
10
10
|
import { pluginFactory } from './vite.js'
|
|
11
11
|
|
|
12
12
|
const ssrEntry = fileURLToPath(new URL('./ssr.jsx', import.meta.url))
|
|
13
|
+
/**
|
|
14
|
+
* Prefix of emitted asset paths in server-rendered markup, replaced per page
|
|
15
|
+
* while prerendering (see `prerender.js`).
|
|
16
|
+
*/
|
|
17
|
+
export const assetUrlMarker = '/@orga-build/asset/'
|
|
13
18
|
|
|
14
19
|
const require = createRequire(import.meta.url)
|
|
15
|
-
const defaultIndexHtml = fileURLToPath(new URL('./index.html', import.meta.url))
|
|
16
20
|
/**
|
|
17
|
-
* Alias map for React
|
|
21
|
+
* Alias map for React to ensure a single copy is bundled
|
|
18
22
|
*/
|
|
19
23
|
export const alias = {
|
|
20
24
|
react: path.dirname(require.resolve('react/package.json')),
|
|
21
|
-
'react-dom': path.dirname(require.resolve('react-dom/package.json'))
|
|
22
|
-
wouter: path.dirname(require.resolve('wouter'))
|
|
25
|
+
'react-dom': path.dirname(require.resolve('react-dom/package.json'))
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* @typedef {Object} OrgaBuildPluginOptions
|
|
27
30
|
* @property {string} root - Root directory for content files
|
|
28
|
-
* @property {string | undefined} [outDir] - Output directory (
|
|
31
|
+
* @property {string | undefined} [outDir] - Output directory (default: Vite's `build.outDir`)
|
|
29
32
|
* @property {string|string[]} [containerClass] - CSS class(es) to wrap rendered content
|
|
30
|
-
* @property {string[]} [styles] - Global stylesheet URLs to
|
|
33
|
+
* @property {string[]} [styles] - Global stylesheet URLs to link from the HTML shell
|
|
31
34
|
* @property {import('unified').PluggableList} [rehypePlugins] - Extra rehype plugins appended to orga-build defaults
|
|
32
35
|
* @property {string[]} [exclude] - Glob patterns for files to exclude from content scanning
|
|
36
|
+
* @property {string | undefined} [site] - Absolute URL the site is served from, e.g. `https://example.com`
|
|
33
37
|
*/
|
|
34
38
|
|
|
35
39
|
/**
|
|
36
|
-
* Creates the
|
|
37
|
-
*
|
|
40
|
+
* Creates the orga-build plugin preset: everything needed for `vite` (dev SSR)
|
|
41
|
+
* and `vite build` (static site) to work, used by the CLI and usable directly
|
|
42
|
+
* in a `vite.config.js`.
|
|
38
43
|
*
|
|
39
44
|
* @param {OrgaBuildPluginOptions} options
|
|
40
45
|
* @returns {import('vite').PluginOption[]}
|
|
@@ -45,198 +50,143 @@ export function orgaBuildPlugin({
|
|
|
45
50
|
containerClass = [],
|
|
46
51
|
styles = [],
|
|
47
52
|
rehypePlugins = [],
|
|
48
|
-
exclude = []
|
|
53
|
+
exclude = [],
|
|
54
|
+
site
|
|
49
55
|
}) {
|
|
56
|
+
// Virtual modules import content files by path, so it must be absolute.
|
|
57
|
+
root = path.resolve(root)
|
|
58
|
+
site = normalizeSite(site)
|
|
59
|
+
const islands = islandPlugin()
|
|
50
60
|
return [
|
|
61
|
+
configPlugin({ root, outDir }),
|
|
62
|
+
htmlShellPlugin(styles),
|
|
63
|
+
devSsrPlugin(site),
|
|
64
|
+
prerenderPlugin(islands, site),
|
|
65
|
+
islands,
|
|
51
66
|
setupOrga({ containerClass, root, rehypePlugins }),
|
|
52
67
|
react(),
|
|
53
|
-
pluginFactory({ dir: root,
|
|
68
|
+
pluginFactory({ dir: root, exclude, site })
|
|
54
69
|
]
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @param {OrgaBuildPluginOptions & { outDir?: string, vitePlugins?: import('vite').PluginOption[], includeFallbackHtml?: boolean, projectRoot?: string }} options
|
|
62
|
-
* @returns {{ plugins: import('vite').PluginOption[], resolve: { alias: typeof alias } }}
|
|
73
|
+
* Without a trailing slash, so `site + page.slug` is a page's URL.
|
|
74
|
+
* @param {string | undefined} site
|
|
63
75
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
projectRoot = process.cwd(),
|
|
73
|
-
exclude = []
|
|
74
|
-
}) {
|
|
75
|
-
const plugins = [
|
|
76
|
-
...vitePlugins,
|
|
77
|
-
...orgaBuildPlugin({
|
|
78
|
-
root,
|
|
79
|
-
outDir,
|
|
80
|
-
containerClass,
|
|
81
|
-
styles,
|
|
82
|
-
rehypePlugins,
|
|
83
|
-
exclude
|
|
84
|
-
})
|
|
85
|
-
]
|
|
86
|
-
if (includeFallbackHtml) {
|
|
87
|
-
// HTML fallback must be first so it can handle HTML navigation requests
|
|
88
|
-
// before runtime plugins (e.g. Cloudflare) potentially return 404.
|
|
89
|
-
plugins.unshift(htmlFallbackPlugin(projectRoot, styles))
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
plugins,
|
|
93
|
-
resolve: { alias }
|
|
76
|
+
function normalizeSite(site) {
|
|
77
|
+
if (site === undefined) return
|
|
78
|
+
// `localhost:3000` parses too, as a URL with the scheme `localhost:`.
|
|
79
|
+
const url = URL.canParse(site) ? new URL(site) : undefined
|
|
80
|
+
if (url?.protocol !== 'http:' && url?.protocol !== 'https:') {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`orga-build: "site" must be an absolute http(s) URL, e.g. "https://example.com", got "${site}"`
|
|
83
|
+
)
|
|
94
84
|
}
|
|
85
|
+
return url.href.replace(/\/+$/, '')
|
|
95
86
|
}
|
|
96
87
|
|
|
97
88
|
/**
|
|
98
|
-
*
|
|
89
|
+
* Creates the full Vite config options for orga-build.
|
|
99
90
|
*
|
|
100
|
-
* @param {
|
|
101
|
-
* @returns {
|
|
91
|
+
* @param {OrgaBuildPluginOptions & { vitePlugins?: import('vite').PluginOption[] }} options
|
|
92
|
+
* @returns {{ plugins: import('vite').PluginOption[] }}
|
|
102
93
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return true
|
|
107
|
-
} catch {
|
|
108
|
-
return false
|
|
94
|
+
export function createOrgaBuildConfig({ vitePlugins = [], ...options }) {
|
|
95
|
+
return {
|
|
96
|
+
plugins: [...vitePlugins, ...orgaBuildPlugin(options)]
|
|
109
97
|
}
|
|
110
98
|
}
|
|
111
99
|
|
|
112
100
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* - SSR-renders each page on every request using Vite's server module runner
|
|
117
|
-
* - Injects rendered content and page metadata (%orga.*% placeholders) into the template
|
|
118
|
-
* - Falls back to the shell HTML for unknown routes (client-side router handles 404)
|
|
119
|
-
* - Only handles GET/HEAD requests that accept HTML
|
|
120
|
-
* - Does not intercept asset requests
|
|
121
|
-
*
|
|
122
|
-
* @param {string} projectRoot - Project root directory (where orga.config.js lives)
|
|
123
|
-
* @param {string[]} [styles]
|
|
101
|
+
* @param {Object} options
|
|
102
|
+
* @param {string} options.root - Content root
|
|
103
|
+
* @param {string | undefined} options.outDir
|
|
124
104
|
* @returns {import('vite').Plugin}
|
|
125
105
|
*/
|
|
126
|
-
|
|
106
|
+
function configPlugin({ root, outDir }) {
|
|
127
107
|
return {
|
|
128
|
-
name: 'orga-build:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
108
|
+
name: 'orga-build:config',
|
|
109
|
+
config(config, { command }) {
|
|
110
|
+
const clientOutDir = outDir ?? config.build?.outDir ?? 'dist'
|
|
111
|
+
const isBuild = command === 'build'
|
|
112
|
+
const contentDir = path
|
|
113
|
+
.relative(path.resolve(config.root ?? ''), path.resolve(root))
|
|
114
|
+
.split(path.sep)
|
|
115
|
+
.join('/')
|
|
116
|
+
return {
|
|
117
|
+
// HTML is served by orga-build:dev-ssr, not Vite's SPA fallback.
|
|
118
|
+
appType: 'custom',
|
|
119
|
+
// Asset URLs in server-rendered markup are resolved per page while
|
|
120
|
+
// prerendering, so a relative `base` works for nested pages. Left
|
|
121
|
+
// alone when the user renders URLs themselves.
|
|
122
|
+
experimental: config.experimental?.renderBuiltUrl
|
|
123
|
+
? {}
|
|
124
|
+
: {
|
|
125
|
+
renderBuiltUrl(filename, { hostType, ssr }) {
|
|
126
|
+
if (ssr && hostType === 'js') return assetUrlMarker + filename
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
// Make `vite build` build every environment through buildApp.
|
|
130
|
+
builder: {},
|
|
131
|
+
// `resolve.alias` is global, not per-environment, so it is only set
|
|
132
|
+
// for build. In dev it would also reach the SSR module runner: the
|
|
133
|
+
// aliases turn bare specifiers (e.g. 'react') into absolute paths,
|
|
134
|
+
// which bypasses Vite's externalization and evaluates CJS packages
|
|
135
|
+
// inline, without 'module'/'require' globals.
|
|
136
|
+
resolve: isBuild ? { alias } : {},
|
|
137
|
+
build: {
|
|
138
|
+
outDir: clientOutDir,
|
|
139
|
+
// Also tells Vite to stop watching outDir in dev.
|
|
140
|
+
emptyOutDir: true,
|
|
141
|
+
cssCodeSplit: false
|
|
142
|
+
},
|
|
143
|
+
server: { forwardConsole: true },
|
|
144
|
+
environments: {
|
|
145
|
+
client: {
|
|
146
|
+
input: 'index.html',
|
|
147
|
+
// Islands are extra client chunks; prerendering reads their
|
|
148
|
+
// hashed names from the manifest.
|
|
149
|
+
build: { manifest: true },
|
|
150
|
+
optimizeDeps: {
|
|
151
|
+
// Scan pages, layouts and components up front so their deps
|
|
152
|
+
// are pre-bundled at startup instead of triggering a reload
|
|
153
|
+
// when first visited.
|
|
154
|
+
entries: [
|
|
155
|
+
'**/*.html',
|
|
156
|
+
path.posix.join(contentDir, '**/*.{jsx,tsx}')
|
|
157
|
+
],
|
|
158
|
+
// The island runtime lives in orga-build itself (in
|
|
159
|
+
// node_modules once installed), which the scanner never
|
|
160
|
+
// crawls. Pre-bundle its CJS deps explicitly or the browser
|
|
161
|
+
// gets raw CommonJS.
|
|
162
|
+
include: ['react', 'react-dom/client']
|
|
161
163
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
164
|
+
},
|
|
165
|
+
ssr: {
|
|
166
|
+
input: ssrEntry,
|
|
167
|
+
// The built SSR bundle is self-contained so it can be imported
|
|
168
|
+
// from its outDir, which sits outside the site's outDir because
|
|
169
|
+
// it must survive the client build.
|
|
170
|
+
resolve: isBuild ? { noExternal: true } : {},
|
|
171
|
+
build: {
|
|
172
|
+
outDir: path.join(
|
|
173
|
+
path.resolve(config.root ?? ''),
|
|
174
|
+
'node_modules/.orga-build/ssr'
|
|
175
|
+
),
|
|
176
|
+
// Pages live only in this graph now, so their images and CSS
|
|
177
|
+
// must be emitted here; prerendering copies them to the site.
|
|
178
|
+
emitAssets: true,
|
|
179
|
+
minify: false,
|
|
180
|
+
rolldownOptions: {
|
|
181
|
+
output: {
|
|
182
|
+
entryFileNames: '[name].mjs',
|
|
183
|
+
chunkFileNames: '[name]-[hash].mjs'
|
|
184
|
+
}
|
|
185
|
+
}
|
|
175
186
|
}
|
|
176
|
-
const bytes = Buffer.from(await response.arrayBuffer())
|
|
177
|
-
res.end(bytes)
|
|
178
|
-
return
|
|
179
187
|
}
|
|
180
|
-
} catch (e) {
|
|
181
|
-
next(e)
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Only handle browser-like navigation requests.
|
|
186
|
-
// Don't match generic */* accepts to avoid hijacking API requests.
|
|
187
|
-
const accept = req.headers.accept || ''
|
|
188
|
-
if (!accept.includes('text/html')) {
|
|
189
|
-
return next()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Don't intercept asset requests (files with extensions)
|
|
193
|
-
if (pathname !== '/' && /\.\w+$/.test(pathname)) {
|
|
194
|
-
return next()
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
// Import via the runner on each request — the module graph handles
|
|
199
|
-
// HMR invalidation so stale modules are never served.
|
|
200
|
-
const { render, pages } = await runner.import(ssrEntry)
|
|
201
|
-
const content = render(pathname)
|
|
202
|
-
|
|
203
|
-
let html = await fs.readFile(indexHtmlPath, 'utf-8')
|
|
204
|
-
html = await server.transformIndexHtml(url, html)
|
|
205
|
-
|
|
206
|
-
const uniqueCssUrls = [...new Set(styles)]
|
|
207
|
-
if (uniqueCssUrls.length > 0) {
|
|
208
|
-
const cssLinks = uniqueCssUrls
|
|
209
|
-
.map((u) => `<link rel="stylesheet" href="${escapeHtml(u)}">`)
|
|
210
|
-
.join('')
|
|
211
|
-
html = html.replace('</head>', `${cssLinks}</head>`)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (content) {
|
|
215
|
-
const ssr = { routePath: pathname }
|
|
216
|
-
html = html.replace(
|
|
217
|
-
'<div id="root"></div>',
|
|
218
|
-
`<script>window._ssr=${JSON.stringify(ssr)};</script><div id="root">${content}</div>`
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Replace %orga.*% placeholders with page metadata
|
|
223
|
-
const page = pages[pathname]
|
|
224
|
-
if (page) {
|
|
225
|
-
html = html.replace(/%orga\.(\w+)%/g, (_, key) => {
|
|
226
|
-
const value = page[key] ?? ''
|
|
227
|
-
return escapeHtml(String(value))
|
|
228
|
-
})
|
|
229
|
-
}
|
|
230
|
-
// Strip any remaining unresolved placeholders (unknown route)
|
|
231
|
-
html = html.replace(/%orga\.\w+%/g, '')
|
|
232
|
-
|
|
233
|
-
res.statusCode = 200
|
|
234
|
-
res.setHeader('Content-Type', 'text/html')
|
|
235
|
-
res.end(html)
|
|
236
|
-
} catch (e) {
|
|
237
|
-
next(e)
|
|
238
188
|
}
|
|
239
|
-
}
|
|
189
|
+
}
|
|
240
190
|
}
|
|
241
191
|
}
|
|
242
192
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns `vite build` into a static site build: builds the SSR and client
|
|
3
|
+
* environments, then renders every page and endpoint into the client outDir.
|
|
4
|
+
*
|
|
5
|
+
* @param {import('vite').Plugin} islands - The island plugin, whose `api`
|
|
6
|
+
* tells whether the client build ran before islands were discovered
|
|
7
|
+
* @param {string | undefined} site - Absolute site URL, without a trailing slash
|
|
8
|
+
* @returns {import('vite').Plugin}
|
|
9
|
+
*/
|
|
10
|
+
export function prerenderPlugin(islands: import("vite").Plugin, site: string | undefined): import("vite").Plugin;
|
|
11
|
+
//# sourceMappingURL=prerender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prerender.d.ts","sourceRoot":"","sources":["prerender.js"],"names":[],"mappings":"AAUA;;;;;;;;GAQG;AACH,yCALW,OAAO,MAAM,EAAE,MAAM,QAErB,MAAM,GAAG,SAAS,GAChB,OAAO,MAAM,EAAE,MAAM,CAsBjC"}
|
package/lib/prerender.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { normalizePath } from 'vite'
|
|
5
|
+
import { resolveEndpointResponse } from './endpoint.js'
|
|
6
|
+
import { ensureDir } from './fs.js'
|
|
7
|
+
import { renderPageHtml } from './html.js'
|
|
8
|
+
import { clientRuntime } from './island.js'
|
|
9
|
+
import { assetUrlMarker } from './plugin.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Turns `vite build` into a static site build: builds the SSR and client
|
|
13
|
+
* environments, then renders every page and endpoint into the client outDir.
|
|
14
|
+
*
|
|
15
|
+
* @param {import('vite').Plugin} islands - The island plugin, whose `api`
|
|
16
|
+
* tells whether the client build ran before islands were discovered
|
|
17
|
+
* @param {string | undefined} site - Absolute site URL, without a trailing slash
|
|
18
|
+
* @returns {import('vite').Plugin}
|
|
19
|
+
*/
|
|
20
|
+
export function prerenderPlugin(islands, site) {
|
|
21
|
+
return {
|
|
22
|
+
name: 'orga-build:prerender',
|
|
23
|
+
|
|
24
|
+
buildApp: {
|
|
25
|
+
// Run after any configured `builder.buildApp`: a client build empties
|
|
26
|
+
// outDir, so prerendered files must be written last.
|
|
27
|
+
order: 'post',
|
|
28
|
+
async handler(builder) {
|
|
29
|
+
const { client, ssr } = builder.environments
|
|
30
|
+
// Islands are discovered while building `ssr`, so it goes first. A
|
|
31
|
+
// client built earlier (e.g. by another plugin's buildApp) lacks them.
|
|
32
|
+
if (!ssr.isBuilt) await builder.build(ssr)
|
|
33
|
+
if (!client.isBuilt || islands.api?.clientIsStale) {
|
|
34
|
+
await builder.build(client)
|
|
35
|
+
}
|
|
36
|
+
await prerender(client, ssr, islands, site)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {import('vite').BuildEnvironment} client
|
|
44
|
+
* @param {import('vite').BuildEnvironment} ssr
|
|
45
|
+
* @param {import('vite').Plugin} islands - Its `api.ssrAssets` lists the files the SSR build emitted
|
|
46
|
+
* @param {string | undefined} site
|
|
47
|
+
*/
|
|
48
|
+
async function prerender(client, ssr, islands, site) {
|
|
49
|
+
const outDir = resolveOutDir(client)
|
|
50
|
+
const ssrOutDir = resolveOutDir(ssr)
|
|
51
|
+
const { root, base } = client.config
|
|
52
|
+
|
|
53
|
+
// Images and CSS imported by pages are emitted by the SSR build next to its
|
|
54
|
+
// bundle: move them into the site and link the CSS (one file, see
|
|
55
|
+
// `cssCodeSplit`).
|
|
56
|
+
/** @type {string[]} */
|
|
57
|
+
const emitted = [.../** @type {Set<string>} */ (islands.api?.ssrAssets ?? [])]
|
|
58
|
+
await Promise.all(
|
|
59
|
+
emitted.map(async (file) => {
|
|
60
|
+
await ensureDir(path.dirname(path.join(outDir, file)))
|
|
61
|
+
await fs.copyFile(path.join(ssrOutDir, file), path.join(outDir, file))
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
const styles = emitted.filter((file) => file.endsWith('.css'))
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* URL of a file in outDir from a page `depth` directories deep. With a
|
|
68
|
+
* relative `base`, URLs are relative to the page's directory.
|
|
69
|
+
* @param {number} depth
|
|
70
|
+
*/
|
|
71
|
+
const urlAt =
|
|
72
|
+
(depth) =>
|
|
73
|
+
/** @param {string} file */
|
|
74
|
+
(file) =>
|
|
75
|
+
base === './'
|
|
76
|
+
? `${depth ? '../'.repeat(depth) : './'}${file}`
|
|
77
|
+
: base + file
|
|
78
|
+
|
|
79
|
+
// Cache-bust so repeated builds in one process load the fresh bundle.
|
|
80
|
+
const ssrModuleUrl = pathToFileURL(path.join(ssrOutDir, 'ssr.mjs'))
|
|
81
|
+
ssrModuleUrl.search = `t=${Date.now()}`
|
|
82
|
+
const { render, pages, endpoints = {} } = await import(ssrModuleUrl.href)
|
|
83
|
+
|
|
84
|
+
// Vite has processed index.html into the shell: stylesheets are already
|
|
85
|
+
// injected with their hashed names.
|
|
86
|
+
const shellPath = path.join(outDir, 'index.html')
|
|
87
|
+
const template = await fs.readFile(shellPath, 'utf-8')
|
|
88
|
+
if (!pages['/']) await fs.rm(shellPath)
|
|
89
|
+
|
|
90
|
+
// Islands are built as extra client chunks; the manifest has their names.
|
|
91
|
+
/** @type {Record<string, { file: string }>} */
|
|
92
|
+
const manifest = JSON.parse(
|
|
93
|
+
await fs.readFile(path.join(outDir, '.vite/manifest.json'), 'utf-8')
|
|
94
|
+
)
|
|
95
|
+
const runtimeSrc = normalizePath(path.relative(root, clientRuntime))
|
|
96
|
+
|
|
97
|
+
await Promise.all(
|
|
98
|
+
Object.keys(pages).map(async (pathname) => {
|
|
99
|
+
const toUrl = urlAt(pathname.split('/').filter(Boolean).length)
|
|
100
|
+
/** @param {string} src */
|
|
101
|
+
const url = (src) => {
|
|
102
|
+
const file = manifest[src]?.file
|
|
103
|
+
if (!file) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`island "${src}" is missing from the client build: the client environment must be built after ssr`
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
return toUrl(file)
|
|
109
|
+
}
|
|
110
|
+
const html = renderPageHtml(rebaseRelativeUrls(template, pathname), {
|
|
111
|
+
// Emitted assets are marked in the markup (see `renderBuiltUrl`).
|
|
112
|
+
content: render(pathname, url)?.replaceAll(assetUrlMarker, toUrl('')),
|
|
113
|
+
page: pages[pathname],
|
|
114
|
+
islandScript: manifest[runtimeSrc] && url(runtimeSrc),
|
|
115
|
+
styles: styles.map(toUrl)
|
|
116
|
+
})
|
|
117
|
+
const writePath = path.join(
|
|
118
|
+
outDir,
|
|
119
|
+
pathname.replace(/^\//, ''),
|
|
120
|
+
'index.html'
|
|
121
|
+
)
|
|
122
|
+
await ensureDir(path.dirname(writePath))
|
|
123
|
+
await fs.writeFile(writePath, html)
|
|
124
|
+
})
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
await Promise.all(
|
|
128
|
+
Object.keys(endpoints).map(async (route) => {
|
|
129
|
+
const ctx = {
|
|
130
|
+
url: new URL((site ?? 'http://localhost') + route),
|
|
131
|
+
params: {},
|
|
132
|
+
mode: /** @type {'build'} */ ('build'),
|
|
133
|
+
route: { route },
|
|
134
|
+
site
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const response = await resolveEndpointResponse(
|
|
138
|
+
endpoints[route],
|
|
139
|
+
ctx,
|
|
140
|
+
'GET'
|
|
141
|
+
)
|
|
142
|
+
if (response.status < 200 || response.status >= 300) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Endpoint route "${route}" returned non-2xx status during build: ${response.status}`
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Responses may carry emitted asset URLs too (see `renderBuiltUrl`).
|
|
149
|
+
// The marker is ASCII, so the substitution is done on a byte-preserving
|
|
150
|
+
// latin1 view: any charset survives, and untouched bodies stay as is.
|
|
151
|
+
let body = Buffer.from(await response.arrayBuffer())
|
|
152
|
+
if (body.includes(assetUrlMarker)) {
|
|
153
|
+
const toUrl = urlAt(route.split('/').filter(Boolean).length - 1)
|
|
154
|
+
body = Buffer.from(
|
|
155
|
+
body.toString('latin1').replaceAll(assetUrlMarker, toUrl('')),
|
|
156
|
+
'latin1'
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
const writePath = path.join(outDir, route.replace(/^\//, ''))
|
|
160
|
+
await ensureDir(path.dirname(writePath))
|
|
161
|
+
await fs.writeFile(writePath, body)
|
|
162
|
+
})
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* With a relative `base`, Vite writes the shell's URLs relative to the root
|
|
168
|
+
* `index.html` (`./assets/…`). Rebase them for pages in nested directories.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} html
|
|
171
|
+
* @param {string} pathname
|
|
172
|
+
*/
|
|
173
|
+
function rebaseRelativeUrls(html, pathname) {
|
|
174
|
+
const depth = pathname.split('/').filter(Boolean).length
|
|
175
|
+
if (!depth) return html
|
|
176
|
+
return html.replace(/(\s(?:src|href)=["'])\.\//g, `$1${'../'.repeat(depth)}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {import('vite').BuildEnvironment} environment
|
|
181
|
+
*/
|
|
182
|
+
function resolveOutDir(environment) {
|
|
183
|
+
return path.resolve(environment.config.root, environment.config.build.outDir)
|
|
184
|
+
}
|
package/lib/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["serve.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["serve.js"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,8BAJW,OAAO,aAAa,EAAE,MAAM,SAC5B,MAAM,gBACN,MAAM,iBAsBhB"}
|
package/lib/serve.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
1
|
import { createServer } from 'vite'
|
|
3
|
-
import {
|
|
2
|
+
import { createOrgaBuildConfig } from './plugin.js'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Start the development server using native Vite.
|
|
@@ -17,31 +16,14 @@ export async function serve(config, port = 3000, projectRoot = process.cwd()) {
|
|
|
17
16
|
styles: config.styles ?? [],
|
|
18
17
|
rehypePlugins: config.rehypePlugins ?? [],
|
|
19
18
|
vitePlugins: config.vitePlugins,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
exclude: config.exclude ?? []
|
|
19
|
+
exclude: config.exclude ?? [],
|
|
20
|
+
site: config.site
|
|
23
21
|
})
|
|
24
22
|
|
|
25
23
|
const server = await createServer({
|
|
24
|
+
root: projectRoot,
|
|
26
25
|
plugins,
|
|
27
|
-
|
|
28
|
-
// Aliases are scoped to the client environment only.
|
|
29
|
-
// The SSR environment must NOT have these aliases: they convert bare specifiers
|
|
30
|
-
// (e.g. 'react') into absolute paths, which bypasses Vite's fetchModule
|
|
31
|
-
// externalization branch and causes CJS packages to be evaluated inline by
|
|
32
|
-
// ESModulesEvaluator (which has no 'module'/'require' globals).
|
|
33
|
-
environments: {
|
|
34
|
-
client: {
|
|
35
|
-
resolve: /** @type {any} */ ({ alias })
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
server: {
|
|
39
|
-
port,
|
|
40
|
-
strictPort: false,
|
|
41
|
-
watch: {
|
|
42
|
-
ignored: [`${path.resolve(config.outDir)}/**`]
|
|
43
|
-
}
|
|
44
|
-
}
|
|
26
|
+
server: { port, strictPort: false }
|
|
45
27
|
})
|
|
46
28
|
|
|
47
29
|
await server.listen()
|
package/lib/ssr.jsx
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { renderToString } from 'react-dom/server'
|
|
2
|
-
import { Router } from 'wouter'
|
|
3
2
|
import endpoints from '/@orga-build/endpoints'
|
|
4
3
|
import pages from '/@orga-build/pages'
|
|
5
4
|
import { App } from './app.jsx'
|
|
5
|
+
import { beginPage } from './island.jsx'
|
|
6
6
|
|
|
7
7
|
export { pages }
|
|
8
8
|
export { endpoints }
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
+
* Render the page at `url` to static HTML.
|
|
12
|
+
*
|
|
11
13
|
* @param {string} url
|
|
14
|
+
* @param {(src: string) => string} resolveIslandUrl - Maps an island's source
|
|
15
|
+
* path (relative to the Vite root) to the URL the browser imports it from
|
|
12
16
|
*/
|
|
13
|
-
export function render(url) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
const ssrContext = {}
|
|
20
|
-
console.log(`rendering ${url}`)
|
|
21
|
-
return renderToString(
|
|
22
|
-
<Router ssrPath={url} ssrContext={ssrContext}>
|
|
23
|
-
<App />
|
|
24
|
-
</Router>
|
|
25
|
-
)
|
|
17
|
+
export function render(url, resolveIslandUrl) {
|
|
18
|
+
if (!pages[url]) return
|
|
19
|
+
beginPage(resolveIslandUrl)
|
|
20
|
+
return renderToString(<App path={url} />)
|
|
26
21
|
}
|