forked from theatre-js/theatre
-
Notifications
You must be signed in to change notification settings - Fork 0
/
build.ts
343 lines (317 loc) · 10.5 KB
/
build.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import type {BuildOptions} from 'esbuild'
import esbuild from 'esbuild'
import {readdir, readFile, stat, writeFile} from 'fs/promises'
import {mapValues} from 'lodash-es'
import path from 'path'
import React from 'react'
import {renderToStaticMarkup} from 'react-dom/server'
import {ServerStyleSheet} from 'styled-components'
import {definedGlobals} from '../../../theatre/devEnv/definedGlobals'
import {createEsbuildLiveReloadTools} from './createEsbuildLiveReloadTools'
import {createProxyServer} from './createProxyServer'
import {createServerForceClose} from './createServerForceClose'
import {PlaygroundPage} from './home/PlaygroundPage'
import {openForOS} from './openForOS'
import {timer} from './timer'
import {tryMultiplePorts} from './tryMultiplePorts'
const playgroundDir = (folder: string) => path.join(__dirname, '..', folder)
const buildDir = playgroundDir('build')
const srcDir = playgroundDir('src')
const sharedDir = playgroundDir('src/shared')
const personalDir = playgroundDir('src/personal')
const testDir = playgroundDir('src/tests')
export async function start(options: {
/** enable live reload and watching stuff */
dev: boolean
/** make some UI elements predictable by setting the __IS_VISUAL_REGRESSION_TESTING value on window */
isVisualRegressionTesting: boolean
serve?: {
findAvailablePort: boolean
openBrowser: boolean
waitBeforeStartingServer?: Promise<void>
/** defaults to 8080 */
defaultPort?: number
}
}): Promise<{stop(): Promise<void>}> {
const defaultPort = options.serve?.defaultPort ?? 8080
const liveReload =
options.serve && options.dev ? createEsbuildLiveReloadTools() : undefined
type PlaygroundExample = {
useHtml?: string
entryFilePath: string
outDir: string
}
type Groups = {
[group: string]: {
[module: string]: PlaygroundExample
}
}
// Collect all entry directories per module per group
const groups: Groups = await Promise.all(
[sharedDir, personalDir, testDir].map(async (groupDir) =>
readdir(groupDir)
.then(async (groupDirItems) => [
path.basename(groupDir),
await Promise.all(
groupDirItems.map(
async (
moduleDirName,
): Promise<[string, PlaygroundExample | undefined]> => {
const entryKey = path.basename(moduleDirName)
const entryFilePath = path.join(
groupDir,
moduleDirName,
'index.tsx',
)
if (
!(await stat(entryFilePath)
.then((s) => s.isFile())
.catch(() => false))
)
return [entryKey, undefined]
return [
entryKey,
{
// Including your own html file for playground is an experimental feature,
// it's not quite ready for "prime time" and advertising to the masses until
// it properly handles file watching.
// It's good for now, since we can use it for some demos, just make sure that
// you add a comment to the custom index.html file saying that you have to
// restart playground server entirely to see changes.
useHtml: await readFile(
path.join(groupDir, moduleDirName, 'index.html'),
'utf-8',
).catch(() => undefined),
entryFilePath,
outDir: path.join(
buildDir,
path.basename(groupDir),
moduleDirName,
),
},
]
},
),
).then((entries) =>
Object.fromEntries(
entries.filter((entry) => entry[1] !== undefined),
),
),
])
.catch(() =>
// If the group dir doesn't exist, we just set its entry to undefined
[path.basename(groupDir), undefined],
),
),
)
.then((entries) =>
Object.fromEntries(
// and then filter it out.
entries.filter((entry) => entry[1] !== undefined),
),
)
.catch(wrapCatch('reading group dirs'))
// Collect all entry files
const entryPoints = Object.values(groups)
.flatMap((group) => Object.values(group))
.map((module) => module.entryFilePath)
// Collect all output directories
const outModules = Object.values(groups).flatMap((group) =>
Object.values(group),
)
// Render home page contents
const homeHtml = (() => {
const sheet = new ServerStyleSheet()
try {
const html = renderToStaticMarkup(
sheet.collectStyles(
React.createElement(PlaygroundPage, {
groups: mapValues(groups, (group) => Object.keys(group)),
}),
),
)
const styleTags = sheet.getStyleTags() // or sheet.getStyleElement();
sheet.seal()
return {
head: styleTags,
html,
}
} catch (error) {
// handle error
console.error(error)
sheet.seal()
process.exit(1)
}
})()
const _initialBuild = timer('esbuild initial playground entry point builds')
const esbuildConfig: BuildOptions = {
entryPoints,
bundle: true,
sourcemap: true,
outdir: buildDir,
target: ['firefox88'],
loader: {
'.png': 'file',
'.glb': 'file',
'.gltf': 'file',
'.svg': 'dataurl',
},
define: {
...definedGlobals,
'window.__IS_VISUAL_REGRESSION_TESTING': JSON.stringify(
options.isVisualRegressionTesting,
),
},
banner: liveReload?.esbuildBanner,
watch: liveReload?.esbuildWatch && {
onRebuild(error, result) {
esbuildWatchStop = result?.stop ?? esbuildWatchStop
liveReload?.esbuildWatch.onRebuild?.(error, result)
},
},
plugins: [
{
name: 'watch playground assets',
setup(build) {
build.onStart(() => {})
build.onLoad(
{
filter: /index\.tsx?$/,
},
async (loadFile) => {
const indexHtmlPath = loadFile.path.replace(
/index\.tsx?$/,
'index.html',
)
const relToSrc = path.relative(srcDir, indexHtmlPath)
const isInSrcFolder = !relToSrc.startsWith('..')
if (isInSrcFolder) {
const newHtml = await readFile(indexHtmlPath, 'utf-8').catch(
() => undefined,
)
if (newHtml) {
await writeFile(
path.resolve(buildDir, relToSrc),
newHtml.replace(
/<\/body>/,
`<script src="${path.join(
'/',
relToSrc,
'../index.js',
)}"></script></body>`,
),
).catch(
wrapCatch(
`loading index.tsx creates corresponding index.html for ${relToSrc}`,
),
)
}
return {
watchFiles: [indexHtmlPath],
}
}
},
)
},
},
],
}
let esbuildWatchStop: undefined | (() => void)
await esbuild
.build(esbuildConfig)
.finally(() => _initialBuild.stop())
.catch(
// if in dev mode, permit continuing to watch even if there was an error
options.dev
? () => Promise.resolve()
: wrapCatch(`failed initial esbuild.build`),
)
.then(async (buildResult) => {
esbuildWatchStop = buildResult?.stop
// Read index.html template
const index = await readFile(
path.join(__dirname, 'index.html'),
'utf8',
).catch(wrapCatch('reading index.html template'))
await Promise.all([
// Write home page
writeFile(
path.join(buildDir, 'index.html'),
index
.replace(/<\/head>/, `${homeHtml.head}<\/head>`)
.replace(/<body>/, `<body>${homeHtml.html}`),
'utf-8',
).catch(wrapCatch('writing build index.html')),
// Write module pages
...outModules.map((outModule) =>
writeFile(
path.join(outModule.outDir, 'index.html'),
// Insert the script
(outModule.useHtml ?? index).replace(
/<\/body>/,
`<script src="${path.join(
'/',
path.relative(buildDir, outModule.outDir),
'index.js',
)}"></script></body>`,
),
'utf-8',
).catch(
wrapCatch(
`writing index.html for ${path.relative(
buildDir,
outModule.outDir,
)}`,
),
),
),
])
})
.catch((err) => {
console.error('build.ts: esbuild or html files writing error', err)
return process.exit(1)
})
// Only start dev server in serve, otherwise just run build and that's it
if (!options.serve) {
return {
stop() {
esbuildWatchStop?.()
return Promise.resolve()
},
}
}
const {serve} = options
await serve.waitBeforeStartingServer
// We start ESBuild serve with no build config because it doesn't need to build
// anything, we are already using ESBuild watch.
/** See https://esbuild.github.io/api/#serve-return-values */
const esbuildServe = await esbuild.serve({servedir: buildDir}, {})
const proxyServer = createProxyServer(liveReload?.handleRequest, {
hostname: '0.0.0.0',
port: esbuildServe.port,
})
const proxyForceExit = createServerForceClose(proxyServer)
const portTries = serve.findAvailablePort ? 10 : 1
const portChosen = await tryMultiplePorts(defaultPort, portTries, proxyServer)
const hostedAt = `http://localhost:${portChosen}`
console.log('Playground running at', hostedAt)
if (serve.openBrowser) {
setTimeout(() => {
if (!liveReload?.hasOpenConnections()) openForOS(hostedAt)
}, 1000)
}
return {
stop() {
esbuildServe.stop()
esbuildWatchStop?.()
return Promise.all([proxyForceExit(), esbuildServe.wait]).then(() => {
// map to void for type defs
})
},
}
}
function wrapCatch(message: string) {
return (err: any) => {
return Promise.reject(`Rejected "${message}":\n ${err.toString()}`)
}
}