-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
static-build.ts
327 lines (290 loc) · 10.2 KB
/
static-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
import glob from 'fast-glob';
import fs from 'fs';
import { bgGreen, bgMagenta, black, dim } from 'kleur/colors';
import path from 'path';
import { fileURLToPath } from 'url';
import * as vite from 'vite';
import { BuildInternals, createBuildInternals } from '../../core/build/internal.js';
import { emptyDir, removeDir } from '../../core/fs/index.js';
import { prependForwardSlash } from '../../core/path.js';
import { isModeServerWithNoAdapter } from '../../core/util.js';
import { runHookBuildSetup } from '../../integrations/index.js';
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { info } from '../logger/core.js';
import { getOutDirWithinCwd } from './common.js';
import { generatePages } from './generate.js';
import { trackPageData } from './internal.js';
import type { PageBuildData, StaticBuildOptions } from './types';
import { getTimeStat } from './util.js';
import { vitePluginAnalyzer } from './vite-plugin-analyzer.js';
import { rollupPluginAstroBuildCSS } from './vite-plugin-css.js';
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
import { vitePluginInternals } from './vite-plugin-internals.js';
import { vitePluginPages } from './vite-plugin-pages.js';
import { injectManifest, vitePluginSSR } from './vite-plugin-ssr.js';
export async function staticBuild(opts: StaticBuildOptions) {
const { allPages, settings } = opts;
// Verify this app is buildable.
if (isModeServerWithNoAdapter(opts.settings)) {
throw new Error(`Cannot use \`output: 'server'\` without an adapter.
Install and configure the appropriate server adapter for your final deployment.
Learn more: https://docs.astro.build/en/guides/server-side-rendering/
// Example: astro.config.js
import netlify from '@astrojs/netlify';
export default {
output: 'server',
adapter: netlify(),
}
`);
}
// The pages to be built for rendering purposes.
const pageInput = new Set<string>();
// A map of each page .astro file, to the PageBuildData which contains information
// about that page, such as its paths.
const facadeIdToPageDataMap = new Map<string, PageBuildData>();
// Build internals needed by the CSS plugin
const internals = createBuildInternals();
const timer: Record<string, number> = {};
timer.buildStart = performance.now();
for (const [component, pageData] of Object.entries(allPages)) {
const astroModuleURL = new URL('./' + component, settings.config.root);
const astroModuleId = prependForwardSlash(component);
// Track the page data in internals
trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
pageInput.add(astroModuleId);
facadeIdToPageDataMap.set(fileURLToPath(astroModuleURL), pageData);
}
// Empty out the dist folder, if needed. Vite has a config for doing this
// but because we are running 2 vite builds in parallel, that would cause a race
// condition, so we are doing it ourselves
emptyDir(settings.config.outDir, new Set('.git'));
// Build your project (SSR application code, assets, client JS, etc.)
timer.ssr = performance.now();
info(opts.logging, 'build', `Building ${settings.config.output} entrypoints...`);
await ssrBuild(opts, internals, pageInput);
info(opts.logging, 'build', dim(`Completed in ${getTimeStat(timer.ssr, performance.now())}.`));
const rendererClientEntrypoints = settings.renderers
.map((r) => r.clientEntrypoint)
.filter((a) => typeof a === 'string') as string[];
const clientInput = new Set([
...internals.discoveredHydratedComponents,
...internals.discoveredClientOnlyComponents,
...rendererClientEntrypoints,
...internals.discoveredScripts,
]);
if (settings.scripts.some((script) => script.stage === 'page')) {
clientInput.add(PAGE_SCRIPT_ID);
}
// Run client build first, so the assets can be fed into the SSR rendered version.
timer.clientBuild = performance.now();
await clientBuild(opts, internals, clientInput);
timer.generate = performance.now();
if (settings.config.output === 'static') {
await generatePages(opts, internals);
await cleanSsrOutput(opts);
} else {
// Inject the manifest
await injectManifest(opts, internals);
info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
await ssrMoveAssets(opts);
}
}
async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set<string>) {
const { settings, viteConfig } = opts;
const ssr = settings.config.output === 'server';
const out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(settings.config.outDir);
const viteBuildConfig: vite.InlineConfig = {
...viteConfig,
mode: viteConfig.mode || 'production',
logLevel: opts.viteConfig.logLevel ?? 'error',
build: {
target: 'esnext',
...viteConfig.build,
emptyOutDir: false,
manifest: false,
outDir: fileURLToPath(out),
copyPublicDir: false,
rollupOptions: {
...viteConfig.build?.rollupOptions,
input: [],
output: {
format: 'esm',
chunkFileNames: 'chunks/[name].[hash].mjs',
assetFileNames: 'assets/[name].[hash][extname]',
...viteConfig.build?.rollupOptions?.output,
entryFileNames: opts.buildConfig.serverEntry,
},
},
ssr: true,
// improve build performance
minify: false,
modulePreload: { polyfill: false },
reportCompressedSize: false,
},
plugins: [
vitePluginInternals(input, internals),
vitePluginPages(opts, internals),
rollupPluginAstroBuildCSS({
buildOptions: opts,
internals,
target: 'server',
}),
...(viteConfig.plugins || []),
// SSR needs to be last
settings.config.output === 'server' && vitePluginSSR(internals, settings.adapter!),
vitePluginAnalyzer(internals),
],
envPrefix: 'PUBLIC_',
base: settings.config.base,
};
await runHookBuildSetup({
config: settings.config,
pages: internals.pagesByComponent,
vite: viteBuildConfig,
target: 'server',
logging: opts.logging,
});
return await vite.build(viteBuildConfig);
}
async function clientBuild(
opts: StaticBuildOptions,
internals: BuildInternals,
input: Set<string>
) {
const { settings, viteConfig } = opts;
const timer = performance.now();
const ssr = settings.config.output === 'server';
const out = ssr ? opts.buildConfig.client : settings.config.outDir;
// Nothing to do if there is no client-side JS.
if (!input.size) {
// If SSR, copy public over
if (ssr) {
await copyFiles(settings.config.publicDir, out);
}
return null;
}
info(opts.logging, null, `\n${bgGreen(black(' building client '))}`);
const viteBuildConfig: vite.InlineConfig = {
...viteConfig,
mode: viteConfig.mode || 'production',
logLevel: 'info',
build: {
target: 'esnext',
...viteConfig.build,
emptyOutDir: false,
outDir: fileURLToPath(out),
rollupOptions: {
...viteConfig.build?.rollupOptions,
input: Array.from(input),
output: {
format: 'esm',
entryFileNames: '[name].[hash].js',
chunkFileNames: 'chunks/[name].[hash].js',
assetFileNames: 'assets/[name].[hash][extname]',
...viteConfig.build?.rollupOptions?.output,
},
preserveEntrySignatures: 'exports-only',
},
},
plugins: [
vitePluginInternals(input, internals),
vitePluginHoistedScripts(settings, internals),
rollupPluginAstroBuildCSS({
buildOptions: opts,
internals,
target: 'client',
}),
...(viteConfig.plugins || []),
],
envPrefix: 'PUBLIC_',
base: settings.config.base,
};
await runHookBuildSetup({
config: settings.config,
pages: internals.pagesByComponent,
vite: viteBuildConfig,
target: 'client',
logging: opts.logging,
});
const buildResult = await vite.build(viteBuildConfig);
info(opts.logging, null, dim(`Completed in ${getTimeStat(timer, performance.now())}.\n`));
return buildResult;
}
async function cleanSsrOutput(opts: StaticBuildOptions) {
const out = getOutDirWithinCwd(opts.settings.config.outDir);
// The SSR output is all .mjs files, the client output is not.
const files = await glob('**/*.mjs', {
cwd: fileURLToPath(out),
});
if (files.length) {
// Remove all the SSR generated .mjs files
await Promise.all(
files.map(async (filename) => {
const url = new URL(filename, out);
await fs.promises.rm(url);
})
);
// Map directories heads from the .mjs files
const directories: Set<string> = new Set();
files.forEach((i) => {
const splitFilePath = i.split(path.sep);
// If the path is more than just a .mjs filename itself
if (splitFilePath.length > 1) {
directories.add(splitFilePath[0]);
}
});
// Attempt to remove only those folders which are empty
await Promise.all(
Array.from(directories).map(async (filename) => {
const url = new URL(filename, out);
const folder = await fs.promises.readdir(url);
if (!folder.length) {
await fs.promises.rm(url, { recursive: true, force: true });
}
})
);
}
// Clean out directly if the outDir is outside of root
if (out.toString() !== opts.settings.config.outDir.toString()) {
// Copy assets before cleaning directory if outside root
copyFiles(out, opts.settings.config.outDir);
await fs.promises.rm(out, { recursive: true });
return;
}
}
async function copyFiles(fromFolder: URL, toFolder: URL) {
const files = await glob('**/*', {
cwd: fileURLToPath(fromFolder),
});
await Promise.all(
files.map(async (filename) => {
const from = new URL(filename, fromFolder);
const to = new URL(filename, toFolder);
const lastFolder = new URL('./', to);
return fs.promises
.mkdir(lastFolder, { recursive: true })
.then(() => fs.promises.copyFile(from, to));
})
);
}
async function ssrMoveAssets(opts: StaticBuildOptions) {
info(opts.logging, 'build', 'Rearranging server assets...');
const serverRoot =
opts.settings.config.output === 'static' ? opts.buildConfig.client : opts.buildConfig.server;
const clientRoot = opts.buildConfig.client;
const serverAssets = new URL('./assets/', serverRoot);
const clientAssets = new URL('./assets/', clientRoot);
const files = await glob('assets/**/*', {
cwd: fileURLToPath(serverRoot),
});
// Make the directory
await fs.promises.mkdir(clientAssets, { recursive: true });
await Promise.all(
files.map(async (filename) => {
const currentUrl = new URL(filename, serverRoot);
const clientUrl = new URL(filename, clientRoot);
return fs.promises.rename(currentUrl, clientUrl);
})
);
removeDir(serverAssets);
}