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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { describe, test } from 'node:test'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { createServer } from 'vite'
|
|
7
|
+
import { orgaBuildPlugin } from '../plugin.js'
|
|
8
|
+
import { writeIslandFixture } from './fixtures.js'
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
|
|
12
|
+
// Separate from the build tests: a Vite build sets NODE_ENV=production for the
|
|
13
|
+
// process, which makes React's dev JSX runtime unusable in a dev server after it.
|
|
14
|
+
describe('orga-build dev', () => {
|
|
15
|
+
test('serves islands in dev under the configured base', async () => {
|
|
16
|
+
const dir = path.join(__dirname, 'fixtures-dev')
|
|
17
|
+
await writeIslandFixture(dir)
|
|
18
|
+
const server = await createServer({
|
|
19
|
+
root: dir,
|
|
20
|
+
configFile: false,
|
|
21
|
+
logLevel: 'silent',
|
|
22
|
+
base: '/site/',
|
|
23
|
+
server: { port: 5179 },
|
|
24
|
+
plugins: orgaBuildPlugin({ root: dir })
|
|
25
|
+
})
|
|
26
|
+
try {
|
|
27
|
+
await server.listen()
|
|
28
|
+
const pageUrl = new URL(server.resolvedUrls?.local[0] ?? '')
|
|
29
|
+
const html = await (
|
|
30
|
+
await fetch(pageUrl, { headers: { accept: 'text/html' } })
|
|
31
|
+
).text()
|
|
32
|
+
const islandSrc = html.match(/<orga-island [^>]*src="([^"]+)"/)?.[1] ?? ''
|
|
33
|
+
const script =
|
|
34
|
+
html.match(/<script type="module" src="([^"]*island[^"]*\.js)"/)?.[1] ??
|
|
35
|
+
''
|
|
36
|
+
assert.equal(islandSrc, '/site/_components.tsx', html)
|
|
37
|
+
assert.equal(script, '/site/@orga-build/islands.js')
|
|
38
|
+
const nested = await (
|
|
39
|
+
await fetch(new URL('docs/', pageUrl), {
|
|
40
|
+
headers: { accept: 'text/html' }
|
|
41
|
+
})
|
|
42
|
+
).text()
|
|
43
|
+
assert.match(nested, /client="visible"/, 'trailing slash finds the page')
|
|
44
|
+
const missing = await fetch(new URL('nope', pageUrl), {
|
|
45
|
+
headers: { accept: 'text/html' }
|
|
46
|
+
})
|
|
47
|
+
assert.equal(missing.status, 404, 'unknown routes are not rendered')
|
|
48
|
+
for (const [url, marker] of [
|
|
49
|
+
[islandSrc, 'Counter'],
|
|
50
|
+
[script, 'hydrateRoot']
|
|
51
|
+
]) {
|
|
52
|
+
const response = await fetch(new URL(url, pageUrl))
|
|
53
|
+
assert.equal(response.status, 200, url)
|
|
54
|
+
assert.ok((await response.text()).includes(marker), url)
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
await server.close()
|
|
58
|
+
await fs.rm(dir, { recursive: true, force: true })
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('serves draft pages for preview', async () => {
|
|
63
|
+
const dir = path.join(__dirname, 'fixtures-dev-drafts')
|
|
64
|
+
await fs.mkdir(dir, { recursive: true })
|
|
65
|
+
await fs.writeFile(path.join(dir, 'draft.org'), '#+draft: t\n\nWIP')
|
|
66
|
+
const server = await createServer({
|
|
67
|
+
root: dir,
|
|
68
|
+
configFile: false,
|
|
69
|
+
logLevel: 'silent',
|
|
70
|
+
server: { port: 5181 },
|
|
71
|
+
plugins: orgaBuildPlugin({ root: dir })
|
|
72
|
+
})
|
|
73
|
+
try {
|
|
74
|
+
await server.listen()
|
|
75
|
+
const url = new URL('draft', server.resolvedUrls?.local[0])
|
|
76
|
+
const html = await (
|
|
77
|
+
await fetch(url, { headers: { accept: 'text/html' } })
|
|
78
|
+
).text()
|
|
79
|
+
assert.ok(html.includes('WIP'), html)
|
|
80
|
+
} finally {
|
|
81
|
+
await server.close()
|
|
82
|
+
await fs.rm(dir, { recursive: true, force: true })
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('links page CSS and reloads the browser on server-rendered edits', async () => {
|
|
87
|
+
const dir = path.join(__dirname, 'fixtures-dev-reload')
|
|
88
|
+
await fs.mkdir(dir, { recursive: true })
|
|
89
|
+
await fs.writeFile(path.join(dir, 'index.org'), '#+title: Home\n\nfirst\n')
|
|
90
|
+
await fs.writeFile(path.join(dir, 'page.css'), '.page { color: red }\n')
|
|
91
|
+
await fs.writeFile(
|
|
92
|
+
path.join(dir, 'page.tsx'),
|
|
93
|
+
`import './page.css'
|
|
94
|
+
export default function Page() {
|
|
95
|
+
return <p className="page">hi</p>
|
|
96
|
+
}
|
|
97
|
+
`
|
|
98
|
+
)
|
|
99
|
+
const server = await createServer({
|
|
100
|
+
root: dir,
|
|
101
|
+
configFile: false,
|
|
102
|
+
logLevel: 'silent',
|
|
103
|
+
server: { port: 5180 },
|
|
104
|
+
plugins: orgaBuildPlugin({ root: dir })
|
|
105
|
+
})
|
|
106
|
+
try {
|
|
107
|
+
await server.listen()
|
|
108
|
+
const pageUrl = new URL(server.resolvedUrls?.local[0] ?? '')
|
|
109
|
+
const get = async (url = '') =>
|
|
110
|
+
(
|
|
111
|
+
await fetch(new URL(url, pageUrl), {
|
|
112
|
+
headers: { accept: 'text/html' }
|
|
113
|
+
})
|
|
114
|
+
).text()
|
|
115
|
+
|
|
116
|
+
const page = await get('page')
|
|
117
|
+
assert.match(page, /<link rel="stylesheet" href="\/page\.css">/)
|
|
118
|
+
const css = await fetch(new URL('page.css', pageUrl), {
|
|
119
|
+
headers: { accept: 'text/css' }
|
|
120
|
+
})
|
|
121
|
+
assert.ok((await css.text()).includes('.page'))
|
|
122
|
+
|
|
123
|
+
/** @type {unknown[]} */
|
|
124
|
+
const sent = []
|
|
125
|
+
server.environments.client.hot.send = (/** @type {any} */ payload) => {
|
|
126
|
+
sent.push(payload)
|
|
127
|
+
}
|
|
128
|
+
await fs.writeFile(
|
|
129
|
+
path.join(dir, 'index.org'),
|
|
130
|
+
'#+title: Home\n\nsecond\n'
|
|
131
|
+
)
|
|
132
|
+
for (let i = 0; i < 50 && !sent.length; i++) {
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
134
|
+
}
|
|
135
|
+
assert.ok(
|
|
136
|
+
sent.some(
|
|
137
|
+
(payload) => /** @type {any} */ (payload).type === 'full-reload'
|
|
138
|
+
),
|
|
139
|
+
'browser is told to reload'
|
|
140
|
+
)
|
|
141
|
+
assert.ok(
|
|
142
|
+
(await get()).includes('second'),
|
|
143
|
+
'next request renders the edit'
|
|
144
|
+
)
|
|
145
|
+
} finally {
|
|
146
|
+
await server.close()
|
|
147
|
+
await fs.rm(dir, { recursive: true, force: true })
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('picks up added, renamed and deleted pages', async () => {
|
|
152
|
+
const site = await startSite('fixtures-dev-pages', 5181, {
|
|
153
|
+
'index.org': '#+title: Home\n\nhome\n'
|
|
154
|
+
})
|
|
155
|
+
try {
|
|
156
|
+
await site.write('new.org', '#+title: New\n\nfresh page\n')
|
|
157
|
+
await site.until('new', (html) => html.includes('fresh page'))
|
|
158
|
+
|
|
159
|
+
await site.rename('new.org', 'renamed.org')
|
|
160
|
+
await site.until('renamed', (html) => html.includes('fresh page'))
|
|
161
|
+
await site.until('new', (html) => !html.includes('fresh page'))
|
|
162
|
+
|
|
163
|
+
// A conflicting route is an error response; the server keeps running.
|
|
164
|
+
await site.write('renamed.tsx', 'export default () => <p>tsx</p>\n')
|
|
165
|
+
await site.until(
|
|
166
|
+
'renamed',
|
|
167
|
+
(html, status) => status === 500 && html.includes('Route conflict')
|
|
168
|
+
)
|
|
169
|
+
await site.remove('renamed.tsx')
|
|
170
|
+
await site.until('renamed', (html) => html.includes('fresh page'))
|
|
171
|
+
|
|
172
|
+
await site.remove('renamed.org')
|
|
173
|
+
await site.until('renamed', (html) => !html.includes('fresh page'))
|
|
174
|
+
assert.ok((await site.get()).html.includes('home'))
|
|
175
|
+
} finally {
|
|
176
|
+
await site.close()
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('picks up added, edited and deleted layouts and components', async () => {
|
|
181
|
+
const site = await startSite('fixtures-dev-layout', 5182, {
|
|
182
|
+
'index.org': '#+title: Home\n\nhome\n',
|
|
183
|
+
'greet.org': '#+title: Greet\n\n#+jsx: <Greeting />\n'
|
|
184
|
+
})
|
|
185
|
+
const layout = (/** @type {string} */ name) =>
|
|
186
|
+
`export default function Layout({ children }) {
|
|
187
|
+
return <main className="${name}">{children}</main>
|
|
188
|
+
}
|
|
189
|
+
`
|
|
190
|
+
const greeting = (/** @type {string} */ text) =>
|
|
191
|
+
`export function Greeting() {
|
|
192
|
+
return <b>${text}</b>
|
|
193
|
+
}
|
|
194
|
+
`
|
|
195
|
+
try {
|
|
196
|
+
await site.write('_layout.tsx', layout('first'))
|
|
197
|
+
await site.until('', (html) => html.includes('<main class="first">'))
|
|
198
|
+
await site.write('_layout.tsx', layout('second'))
|
|
199
|
+
await site.until('', (html) => html.includes('<main class="second">'))
|
|
200
|
+
await site.remove('_layout.tsx')
|
|
201
|
+
await site.until('', (html) => !html.includes('<main'))
|
|
202
|
+
|
|
203
|
+
await site.write('_components.tsx', greeting('hello'))
|
|
204
|
+
await site.until('greet', (html) => html.includes('<b>hello</b>'))
|
|
205
|
+
await site.write('_components.tsx', greeting('bye'))
|
|
206
|
+
await site.until('greet', (html) => html.includes('<b>bye</b>'))
|
|
207
|
+
await site.remove('_components.tsx')
|
|
208
|
+
await site.until(
|
|
209
|
+
'',
|
|
210
|
+
(html, status) => status === 200 && html.includes('home')
|
|
211
|
+
)
|
|
212
|
+
} finally {
|
|
213
|
+
await site.close()
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('getPages reflects metadata edits', async () => {
|
|
218
|
+
const site = await startSite('fixtures-dev-content', 5183, {
|
|
219
|
+
'post.org': '#+title: First\n\npost\n',
|
|
220
|
+
'list.tsx': `import { getPages } from 'orga-build:content'
|
|
221
|
+
export default function List() {
|
|
222
|
+
return <ul>{getPages().map((p) => <li key={p.slug}>{String(p.data.title)}</li>)}</ul>
|
|
223
|
+
}
|
|
224
|
+
`
|
|
225
|
+
})
|
|
226
|
+
try {
|
|
227
|
+
await site.until('list', (html) => html.includes('<li>First</li>'))
|
|
228
|
+
await site.write('post.org', '#+title: Second\n\npost\n')
|
|
229
|
+
await site.until('list', (html) => html.includes('<li>Second</li>'))
|
|
230
|
+
await site.write('other.org', '#+title: Other\n')
|
|
231
|
+
await site.until('list', (html) => html.includes('<li>Other</li>'))
|
|
232
|
+
} finally {
|
|
233
|
+
await site.close()
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Starts a dev server on a fresh fixture directory.
|
|
240
|
+
* @param {string} name
|
|
241
|
+
* @param {number} port
|
|
242
|
+
* @param {Record<string, string>} initial - file contents by relative path
|
|
243
|
+
*/
|
|
244
|
+
async function startSite(name, port, initial) {
|
|
245
|
+
const dir = path.join(__dirname, name)
|
|
246
|
+
await fs.rm(dir, { recursive: true, force: true })
|
|
247
|
+
await fs.mkdir(dir, { recursive: true })
|
|
248
|
+
for (const [file, content] of Object.entries(initial)) {
|
|
249
|
+
await fs.writeFile(path.join(dir, file), content)
|
|
250
|
+
}
|
|
251
|
+
const server = await createServer({
|
|
252
|
+
root: dir,
|
|
253
|
+
configFile: false,
|
|
254
|
+
logLevel: 'silent',
|
|
255
|
+
server: { port },
|
|
256
|
+
plugins: orgaBuildPlugin({ root: dir })
|
|
257
|
+
})
|
|
258
|
+
await server.listen()
|
|
259
|
+
const pageUrl = new URL(server.resolvedUrls?.local[0] ?? '')
|
|
260
|
+
|
|
261
|
+
async function get(url = '') {
|
|
262
|
+
const response = await fetch(new URL(url, pageUrl), {
|
|
263
|
+
headers: { accept: 'text/html' }
|
|
264
|
+
})
|
|
265
|
+
return { status: response.status, html: await response.text() }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
get,
|
|
270
|
+
/** @param {string} file @param {string} content */
|
|
271
|
+
write: (file, content) => fs.writeFile(path.join(dir, file), content),
|
|
272
|
+
/** @param {string} file */
|
|
273
|
+
remove: (file) => fs.rm(path.join(dir, file)),
|
|
274
|
+
/** @param {string} from @param {string} to */
|
|
275
|
+
rename: (from, to) => fs.rename(path.join(dir, from), path.join(dir, to)),
|
|
276
|
+
/**
|
|
277
|
+
* Polls `url` until `check` passes, as a browser would after reloading.
|
|
278
|
+
* @param {string} url
|
|
279
|
+
* @param {(html: string, status: number) => boolean} check
|
|
280
|
+
*/
|
|
281
|
+
async until(url, check) {
|
|
282
|
+
let last = { status: 0, html: '' }
|
|
283
|
+
for (let i = 0; i < 50; i++) {
|
|
284
|
+
last = await get(url)
|
|
285
|
+
if (check(last.html, last.status)) return
|
|
286
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
287
|
+
}
|
|
288
|
+
assert.fail(`/${url} never matched; last ${last.status}:\n${last.html}`)
|
|
289
|
+
},
|
|
290
|
+
async close() {
|
|
291
|
+
await server.close()
|
|
292
|
+
await fs.rm(dir, { recursive: true, force: true })
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A site with a `'use client'` component used from org pages: `/` hydrates it
|
|
3
|
+
* on load, `/docs` when visible (plus a `memo()` export), and `/plain` has no
|
|
4
|
+
* island. `/page` is a whole-page island.
|
|
5
|
+
* @param {string} dir
|
|
6
|
+
*/
|
|
7
|
+
export function writeIslandFixture(dir: string): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=fixtures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["fixtures.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wCAFW,MAAM,iBAqDhB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A site with a `'use client'` component used from org pages: `/` hydrates it
|
|
6
|
+
* on load, `/docs` when visible (plus a `memo()` export), and `/plain` has no
|
|
7
|
+
* island. `/page` is a whole-page island.
|
|
8
|
+
* @param {string} dir
|
|
9
|
+
*/
|
|
10
|
+
export async function writeIslandFixture(dir) {
|
|
11
|
+
await fs.mkdir(path.join(dir, 'docs'), { recursive: true })
|
|
12
|
+
await fs.writeFile(
|
|
13
|
+
path.join(dir, '_components.tsx'),
|
|
14
|
+
`'use client'
|
|
15
|
+
import { memo, useId } from 'react'
|
|
16
|
+
import { useCounter } from './_hooks'
|
|
17
|
+
|
|
18
|
+
export function Counter({ start }: { start: number }) {
|
|
19
|
+
const [count, setCount] = useCounter(start)
|
|
20
|
+
return (
|
|
21
|
+
<button id={useId()} onClick={() => setCount(count + 1)}>
|
|
22
|
+
{count}
|
|
23
|
+
</button>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const MemoCounter = memo(Counter)
|
|
28
|
+
`
|
|
29
|
+
)
|
|
30
|
+
await fs.writeFile(
|
|
31
|
+
path.join(dir, 'index.org'),
|
|
32
|
+
'#+title: Home\n\n#+jsx: <Counter start={1} />\n'
|
|
33
|
+
)
|
|
34
|
+
await fs.writeFile(
|
|
35
|
+
path.join(dir, 'docs', 'index.org'),
|
|
36
|
+
'#+title: Docs\n\n#+jsx: <Counter start={2} client="visible" />\n\n#+jsx: <MemoCounter start={3} meta={{ children: ["a"], skipped: undefined }} />\n\n[[../pic.png]]\n'
|
|
37
|
+
)
|
|
38
|
+
// Larger than Vite's inline limit, so it is emitted as a file.
|
|
39
|
+
await fs.writeFile(path.join(dir, 'pic.png'), Buffer.alloc(8192, 1))
|
|
40
|
+
// A hook imported from one 'use client' module into another must stay a hook.
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.join(dir, '_hooks.tsx'),
|
|
43
|
+
`'use client'
|
|
44
|
+
import { useState } from 'react'
|
|
45
|
+
|
|
46
|
+
export function useCounter(start: number) {
|
|
47
|
+
return useState(start)
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
)
|
|
51
|
+
await fs.writeFile(path.join(dir, 'plain.org'), '#+title: Plain\n\nText.\n')
|
|
52
|
+
await fs.writeFile(
|
|
53
|
+
path.join(dir, 'page.tsx'),
|
|
54
|
+
`'use client'
|
|
55
|
+
export const title = 'Page'
|
|
56
|
+
export default function Page() {
|
|
57
|
+
return <button onClick={() => alert('hi')}>page</button>
|
|
58
|
+
}
|
|
59
|
+
`
|
|
60
|
+
)
|
|
61
|
+
}
|
package/lib/app.jsx
CHANGED
|
@@ -1,56 +1,29 @@
|
|
|
1
|
-
import { Link, Route, Switch } from 'wouter'
|
|
2
|
-
import * as components from '/@orga-build/components'
|
|
3
1
|
import layouts from '/@orga-build/layouts'
|
|
4
2
|
import pages from '/@orga-build/pages'
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
...page
|
|
18
|
-
}
|
|
19
|
-
})
|
|
4
|
+
/**
|
|
5
|
+
* The page at `path`, wrapped in every `_layout` above it.
|
|
6
|
+
*
|
|
7
|
+
* @param {{ path: string }} props
|
|
8
|
+
*/
|
|
9
|
+
export function App({ path }) {
|
|
10
|
+
const page = pages[path]
|
|
11
|
+
const _pages = Object.entries(pages).map(([slug, page]) => ({
|
|
12
|
+
slug,
|
|
13
|
+
...page
|
|
14
|
+
}))
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
let element = <page.default />
|
|
17
|
+
const layoutIds = Object.keys(layouts)
|
|
18
|
+
.filter((key) => path.startsWith(key))
|
|
19
|
+
.sort((a, b) => -a.localeCompare(b))
|
|
20
|
+
for (const layoutId of layoutIds) {
|
|
21
|
+
const Layout = layouts[layoutId]
|
|
22
|
+
element = (
|
|
23
|
+
<Layout title={page.title} slug={path} pages={_pages} {...page}>
|
|
24
|
+
{element}
|
|
25
|
+
</Layout>
|
|
30
26
|
)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
element = (
|
|
34
|
-
<Layout title={page.title} slug={path} pages={_pages} {...page}>
|
|
35
|
-
{element}
|
|
36
|
-
</Layout>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
path,
|
|
41
|
-
element
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<Switch>
|
|
47
|
-
{pageRoutes.map((route) => {
|
|
48
|
-
return (
|
|
49
|
-
<Route key={`r-${route.path}`} path={route.path}>
|
|
50
|
-
{route.element}
|
|
51
|
-
</Route>
|
|
52
|
-
)
|
|
53
|
-
})}
|
|
54
|
-
</Switch>
|
|
55
|
-
)
|
|
27
|
+
}
|
|
28
|
+
return element
|
|
56
29
|
}
|
package/lib/build.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Build the site into `outDir`. The work happens in Vite's `buildApp`, see
|
|
3
|
+
* `prerenderPlugin`.
|
|
4
|
+
*
|
|
2
5
|
* @param {import('./config.js').Config} config
|
|
3
6
|
* @param {string} [projectRoot]
|
|
4
7
|
*/
|
|
5
|
-
export function build({ outDir, root, containerClass, styles, rehypePlugins, vitePlugins, exclude }: import("./config.js").Config, projectRoot?: string): Promise<void>;
|
|
8
|
+
export function build({ outDir, root, containerClass, styles, rehypePlugins, vitePlugins, exclude, site }: import("./config.js").Config, projectRoot?: string): Promise<void>;
|
|
6
9
|
export { alias };
|
|
7
10
|
import { alias } from './plugin.js';
|
|
8
11
|
//# 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":"AAMA;;;;;;GAMG;AACH,2GAHW,OAAO,aAAa,EAAE,MAAM,gBAC5B,MAAM,iBA4BhB;;sBAtC4C,aAAa"}
|
package/lib/build.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
1
|
import { createBuilder } from 'vite'
|
|
5
|
-
import { resolveEndpointResponse } from './endpoint.js'
|
|
6
|
-
import { emptyDir, ensureDir, exists } from './fs.js'
|
|
7
2
|
import { alias, createOrgaBuildConfig } from './plugin.js'
|
|
8
|
-
import { escapeHtml } from './util.js'
|
|
9
|
-
import { appEntryId } from './vite.js'
|
|
10
3
|
|
|
11
4
|
// Re-export alias for backwards compatibility
|
|
12
5
|
export { alias }
|
|
13
6
|
|
|
14
|
-
const ssrEntry = fileURLToPath(new URL('./ssr.jsx', import.meta.url))
|
|
15
|
-
const defaultIndexHtml = fileURLToPath(new URL('./index.html', import.meta.url))
|
|
16
|
-
|
|
17
7
|
/**
|
|
8
|
+
* Build the site into `outDir`. The work happens in Vite's `buildApp`, see
|
|
9
|
+
* `prerenderPlugin`.
|
|
10
|
+
*
|
|
18
11
|
* @param {import('./config.js').Config} config
|
|
19
12
|
* @param {string} [projectRoot]
|
|
20
13
|
*/
|
|
@@ -26,180 +19,22 @@ export async function build(
|
|
|
26
19
|
styles = [],
|
|
27
20
|
rehypePlugins = [],
|
|
28
21
|
vitePlugins = [],
|
|
29
|
-
exclude = []
|
|
22
|
+
exclude = [],
|
|
23
|
+
site
|
|
30
24
|
},
|
|
31
25
|
projectRoot = process.cwd()
|
|
32
26
|
) {
|
|
33
|
-
|
|
34
|
-
const ssrOutDir = path.join(outDir, '.ssr')
|
|
35
|
-
const clientOutDir = outDir
|
|
36
|
-
|
|
37
|
-
const { plugins, resolve } = createOrgaBuildConfig({
|
|
27
|
+
const { plugins } = createOrgaBuildConfig({
|
|
38
28
|
root,
|
|
39
29
|
outDir,
|
|
40
30
|
containerClass,
|
|
41
31
|
styles,
|
|
42
32
|
rehypePlugins,
|
|
43
33
|
vitePlugins,
|
|
44
|
-
exclude
|
|
34
|
+
exclude,
|
|
35
|
+
site
|
|
45
36
|
})
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
plugins,
|
|
50
|
-
resolve,
|
|
51
|
-
ssr: { noExternal: true },
|
|
52
|
-
environments: {
|
|
53
|
-
ssr: {
|
|
54
|
-
build: {
|
|
55
|
-
ssr: true,
|
|
56
|
-
outDir: ssrOutDir,
|
|
57
|
-
cssCodeSplit: false,
|
|
58
|
-
emptyOutDir: true,
|
|
59
|
-
minify: false,
|
|
60
|
-
rollupOptions: {
|
|
61
|
-
input: ssrEntry,
|
|
62
|
-
output: {
|
|
63
|
-
entryFileNames: '[name].mjs',
|
|
64
|
-
chunkFileNames: '[name]-[hash].mjs'
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
client: {
|
|
70
|
-
build: {
|
|
71
|
-
outDir: clientOutDir,
|
|
72
|
-
cssCodeSplit: false,
|
|
73
|
-
emptyOutDir: false,
|
|
74
|
-
assetsDir: 'assets',
|
|
75
|
-
rollupOptions: {
|
|
76
|
-
input: appEntryId,
|
|
77
|
-
preserveEntrySignatures: 'allow-extension'
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// Build SSR first to get render function and pages
|
|
85
|
-
console.log('preparing ssr bundle...')
|
|
86
|
-
await builder.build(builder.environments.ssr)
|
|
87
|
-
|
|
88
|
-
const {
|
|
89
|
-
render,
|
|
90
|
-
pages,
|
|
91
|
-
endpoints = {}
|
|
92
|
-
} = await import(pathToFileURL(path.join(ssrOutDir, 'ssr.mjs')).toString())
|
|
93
|
-
|
|
94
|
-
// Build client bundle
|
|
95
|
-
const _clientResult = await builder.build(builder.environments.client)
|
|
96
|
-
|
|
97
|
-
// Normalize build result to single RollupOutput
|
|
98
|
-
const clientOutput = Array.isArray(_clientResult)
|
|
99
|
-
? _clientResult[0].output
|
|
100
|
-
: 'output' in _clientResult
|
|
101
|
-
? _clientResult.output
|
|
102
|
-
: null
|
|
103
|
-
if (!clientOutput) throw new Error('Unexpected client build result')
|
|
104
|
-
|
|
105
|
-
/* --- get from client bundle result: entry chunk, css chunks --- */
|
|
106
|
-
const entryChunk = clientOutput.find(
|
|
107
|
-
(/** @type {any} */ c) => c.type === 'chunk' && c.isEntry
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
const cssChunks = clientOutput.filter(
|
|
111
|
-
(/** @type {any} */ c) => c.type === 'asset' && c.fileName.endsWith('.css')
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
/* --- get html template, inject entry js and css --- */
|
|
115
|
-
// Check for user's index.html in project root, otherwise use default
|
|
116
|
-
const userIndexPath = path.join(projectRoot, 'index.html')
|
|
117
|
-
const indexHtmlPath = (await exists(userIndexPath))
|
|
118
|
-
? userIndexPath
|
|
119
|
-
: defaultIndexHtml
|
|
120
|
-
const template = await fs.readFile(indexHtmlPath, { encoding: 'utf-8' })
|
|
121
|
-
/* --- for each page path, render html using render function from ssr bundle, and inject the right css --- */
|
|
122
|
-
const pagePaths = Object.keys(pages)
|
|
123
|
-
await Promise.all(
|
|
124
|
-
pagePaths.map(async (pagePath) => {
|
|
125
|
-
const html = renderHTML(pagePath)
|
|
126
|
-
const writePath = path.join(
|
|
127
|
-
clientOutDir,
|
|
128
|
-
pagePath.replace(/^\//, ''),
|
|
129
|
-
'index.html'
|
|
130
|
-
)
|
|
131
|
-
await ensureDir(path.dirname(writePath))
|
|
132
|
-
await fs.writeFile(writePath, html)
|
|
133
|
-
})
|
|
134
|
-
)
|
|
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
|
-
|
|
161
|
-
await fs.rm(ssrOutDir, { recursive: true })
|
|
162
|
-
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
// ---------- the end ----------
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* @param {string} pagePath
|
|
169
|
-
*/
|
|
170
|
-
function renderHTML(pagePath) {
|
|
171
|
-
const content = render(pagePath)
|
|
172
|
-
const ssr = {
|
|
173
|
-
routePath: pagePath
|
|
174
|
-
}
|
|
175
|
-
let html = template.replace(
|
|
176
|
-
'<div id="root"></div>',
|
|
177
|
-
`
|
|
178
|
-
<script>window._ssr=${JSON.stringify(ssr)};</script>
|
|
179
|
-
<div id="root">${content}</div>
|
|
180
|
-
`
|
|
181
|
-
)
|
|
182
|
-
const css = cssChunks
|
|
183
|
-
.map(
|
|
184
|
-
(/** @type {any} */ c) =>
|
|
185
|
-
`<link rel="stylesheet" href="/${c.fileName}">`
|
|
186
|
-
)
|
|
187
|
-
.join('\n')
|
|
188
|
-
html = html.replace(
|
|
189
|
-
'<script type="module" src="/@orga-build/main.js"></script>',
|
|
190
|
-
`<script type="module" src="/${entryChunk?.fileName}"></script>`
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
html = html.replace('</head>', `${css}</head>`)
|
|
194
|
-
|
|
195
|
-
const page = pages[pagePath]
|
|
196
|
-
if (page) {
|
|
197
|
-
html = html.replace(/%orga\.(\w+)%/g, (_, key) => {
|
|
198
|
-
const value = page[key] ?? ''
|
|
199
|
-
return escapeHtml(String(value))
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return html
|
|
204
|
-
}
|
|
38
|
+
const builder = await createBuilder({ root: projectRoot, plugins })
|
|
39
|
+
await builder.buildApp()
|
|
205
40
|
}
|