@prairielearn/compiled-assets 3.0.18 → 3.1.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/CHANGELOG.md +6 -0
- package/README.md +30 -9
- package/dist/cli.js +15 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +92 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/cli.ts +17 -8
- package/src/index.ts +129 -13
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This package enables the transpilation and bundling of client-side assets, namely JavaScript.
|
|
4
4
|
|
|
5
|
-
This tool is meant to produce many small, independent bundles that can then be included as needed on each page.
|
|
5
|
+
This tool is meant to produce many small, independent bundles that can then be included as needed on each page, as well as providing mechanisms for code splitting larger ESM bundles.
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
@@ -20,15 +20,26 @@ Create a directory of assets that you wish to bundle, e.g. `assets/`. Within tha
|
|
|
20
20
|
You can locate shared code in directories inside this directory. As long as those files aren't in the root of the `scripts/` directory, they won't become separate bundles.
|
|
21
21
|
|
|
22
22
|
```text
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
assets
|
|
24
|
+
└── scripts
|
|
25
|
+
├── bar.ts
|
|
26
|
+
├── foo.ts
|
|
27
|
+
└── lib
|
|
28
|
+
├── more-shared-code.ts
|
|
29
|
+
└── shared-code.ts
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
These assets will be output as an [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) for compatibility reasons. You can place additional assets in the `assets/scripts/esm-bundles/` directory, which will be output as ESM [module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#applying_the_module_to_your_html) files with code splitting. This is useful for libraries that can be loaded asynchronously, like Preact components.
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
assets
|
|
36
|
+
└── scripts
|
|
37
|
+
└── esm-bundles
|
|
38
|
+
└── baz.tsx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The import tree of all files will be analyzed, and any code-split chunks or dynamically-imported files will be marked as preloads.
|
|
42
|
+
|
|
32
43
|
### Application integration
|
|
33
44
|
|
|
34
45
|
Early in your application initialization process, initialize this library with the appropriate options:
|
|
@@ -37,6 +48,8 @@ Early in your application initialization process, initialize this library with t
|
|
|
37
48
|
import * as compiledAssets from '@prairielearn/compiled-assets';
|
|
38
49
|
|
|
39
50
|
assets.init({
|
|
51
|
+
// Assets will be watched for changes in development mode, and the latest version will be served.
|
|
52
|
+
// In production mode, assets will be precompiled and served from the build directory.
|
|
40
53
|
dev: process.env.NODE_ENV !== 'production',
|
|
41
54
|
sourceDirectory: './assets',
|
|
42
55
|
buildDirectory: './public/build',
|
|
@@ -54,9 +67,15 @@ app.use('/build/', assets.handler());
|
|
|
54
67
|
|
|
55
68
|
To include a bundle on your page, you can use the `compiledScriptTag` or `compiledScriptPath` functions. The name of the bundle passed to this function is the filename of your bundle within the `scripts` directory.
|
|
56
69
|
|
|
70
|
+
If your file is located in the `esm-bundles` folder (and processed with ESM + code splitting), you can use the `compiledScriptModuleTag` function. You can use `compiledScriptModulePreloadTags` to get a list of tags that should be preloaded in the `<head>` of your HTML document.
|
|
71
|
+
|
|
57
72
|
```ts
|
|
58
73
|
import { html } from '@prairielearn/html';
|
|
59
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
compiledScriptTag,
|
|
76
|
+
compiledScriptPath,
|
|
77
|
+
compiledScriptModuleTag,
|
|
78
|
+
} from '@prairielearn/compiled-assets';
|
|
60
79
|
|
|
61
80
|
router.get(() => {
|
|
62
81
|
return html`
|
|
@@ -64,6 +83,8 @@ router.get(() => {
|
|
|
64
83
|
<head>
|
|
65
84
|
${compiledScriptTag('foo.ts')}
|
|
66
85
|
<script src="${compiledScriptPath('bar.ts')}"></script>
|
|
86
|
+
|
|
87
|
+
${compiledScriptModuleTag('baz.tsx')}
|
|
67
88
|
</head>
|
|
68
89
|
</body>
|
|
69
90
|
Hello, world.
|
package/dist/cli.js
CHANGED
|
@@ -8,16 +8,25 @@ import prettyBytes from 'pretty-bytes';
|
|
|
8
8
|
import { build } from './index.js';
|
|
9
9
|
const gzip = promisify(zlib.gzip);
|
|
10
10
|
const brotli = promisify(zlib.brotliCompress);
|
|
11
|
+
/**
|
|
12
|
+
* Writes gzip and brotli compressed versions of the assets in the specified directory.
|
|
13
|
+
* It reads each asset file, compresses it using gzip and brotli algorithms, and writes the compressed files
|
|
14
|
+
* with appropriate extensions (.gz and .br) in the same directory.
|
|
15
|
+
*
|
|
16
|
+
* @param destination Directory where the compressed assets will be written.
|
|
17
|
+
* @param manifest The assets manifest containing information about the assets.
|
|
18
|
+
* @returns A promise that resolves to an object containing the sizes of the original, gzip, and brotli compressed assets.
|
|
19
|
+
*/
|
|
11
20
|
async function writeCompressedAssets(destination, manifest) {
|
|
12
21
|
const compressedSizes = {};
|
|
13
|
-
await Promise.all(Object.values(manifest).map(async (
|
|
14
|
-
const destinationFilePath = path.resolve(destination,
|
|
22
|
+
await Promise.all(Object.values(manifest).map(async (asset) => {
|
|
23
|
+
const destinationFilePath = path.resolve(destination, asset.assetPath);
|
|
15
24
|
const contents = await fs.readFile(destinationFilePath);
|
|
16
25
|
const gzipCompressed = await gzip(contents);
|
|
17
26
|
const brotliCompressed = await brotli(contents);
|
|
18
27
|
await fs.writeFile(`${destinationFilePath}.gz`, gzipCompressed);
|
|
19
28
|
await fs.writeFile(`${destinationFilePath}.br`, brotliCompressed);
|
|
20
|
-
compressedSizes[
|
|
29
|
+
compressedSizes[asset.assetPath] = {
|
|
21
30
|
raw: contents.length,
|
|
22
31
|
gzip: gzipCompressed.length,
|
|
23
32
|
brotli: brotliCompressed.length,
|
|
@@ -32,10 +41,10 @@ program.command('build <source> <destination>').action(async (source, destinatio
|
|
|
32
41
|
const compressedSizes = await writeCompressedAssets(destination, manifest);
|
|
33
42
|
// Format the output into an object that we can pass to `console.table`.
|
|
34
43
|
const results = {};
|
|
35
|
-
Object.entries(manifest).forEach(([entryPoint,
|
|
36
|
-
const sizes = compressedSizes[assetPath];
|
|
44
|
+
Object.entries(manifest).forEach(([entryPoint, asset]) => {
|
|
45
|
+
const sizes = compressedSizes[asset.assetPath];
|
|
37
46
|
results[entryPoint] = {
|
|
38
|
-
'Output file': assetPath,
|
|
47
|
+
'Output file': asset.assetPath,
|
|
39
48
|
Size: prettyBytes(sizes.raw),
|
|
40
49
|
'Size (gzip)': prettyBytes(sizes.gzip),
|
|
41
50
|
'Size (brotli)': prettyBytes(sizes.brotli),
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,WAAW,MAAM,cAAc,CAAC;AAEvC,OAAO,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport path from 'path';\nimport { promisify } from 'util';\nimport zlib from 'zlib';\n\nimport { program } from 'commander';\nimport fs from 'fs-extra';\nimport prettyBytes from 'pretty-bytes';\n\nimport { type AssetsManifest, build } from './index.js';\n\nconst gzip = promisify(zlib.gzip);\nconst brotli = promisify(zlib.brotliCompress);\n\ntype CompressedSizes = Record<string, Record<string, number>>;\n\n/**\n * Writes gzip and brotli compressed versions of the assets in the specified directory.\n * It reads each asset file, compresses it using gzip and brotli algorithms, and writes the compressed files\n * with appropriate extensions (.gz and .br) in the same directory.\n *\n * @param destination Directory where the compressed assets will be written.\n * @param manifest The assets manifest containing information about the assets.\n * @returns A promise that resolves to an object containing the sizes of the original, gzip, and brotli compressed assets.\n */\nasync function writeCompressedAssets(\n destination: string,\n manifest: AssetsManifest,\n): Promise<CompressedSizes> {\n const compressedSizes: CompressedSizes = {};\n await Promise.all(\n Object.values(manifest).map(async (asset) => {\n const destinationFilePath = path.resolve(destination, asset.assetPath);\n const contents = await fs.readFile(destinationFilePath);\n const gzipCompressed = await gzip(contents);\n const brotliCompressed = await brotli(contents);\n await fs.writeFile(`${destinationFilePath}.gz`, gzipCompressed);\n await fs.writeFile(`${destinationFilePath}.br`, brotliCompressed);\n compressedSizes[asset.assetPath] = {\n raw: contents.length,\n gzip: gzipCompressed.length,\n brotli: brotliCompressed.length,\n };\n }),\n );\n return compressedSizes;\n}\n\nprogram.command('build <source> <destination>').action(async (source, destination) => {\n const manifest = await build(source, destination);\n\n // Write gzip and brotli versions of the output files. Record size information\n // so we can show it to the user.\n const compressedSizes = await writeCompressedAssets(destination, manifest);\n\n // Format the output into an object that we can pass to `console.table`.\n const results: Record<string, any> = {};\n Object.entries(manifest).forEach(([entryPoint, asset]) => {\n const sizes = compressedSizes[asset.assetPath];\n results[entryPoint] = {\n 'Output file': asset.assetPath,\n Size: prettyBytes(sizes.raw),\n 'Size (gzip)': prettyBytes(sizes.gzip),\n 'Size (brotli)': prettyBytes(sizes.brotli),\n };\n });\n console.table(results);\n});\n\nprogram.parseAsync(process.argv).catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
2
|
import { type HtmlSafeString } from '@prairielearn/html';
|
|
3
|
-
type AssetsManifest = Record<string,
|
|
3
|
+
export type AssetsManifest = Record<string, {
|
|
4
|
+
assetPath: string;
|
|
5
|
+
preloads: string[];
|
|
6
|
+
}>;
|
|
4
7
|
export interface CompiledAssetsOptions {
|
|
5
8
|
/**
|
|
6
9
|
* Whether the app is running in dev mode. If dev mode is enabled, then
|
|
@@ -26,5 +29,6 @@ export declare function compiledScriptPath(sourceFile: string): string;
|
|
|
26
29
|
export declare function compiledStylesheetPath(sourceFile: string): string;
|
|
27
30
|
export declare function compiledScriptTag(sourceFile: string): HtmlSafeString;
|
|
28
31
|
export declare function compiledStylesheetTag(sourceFile: string): HtmlSafeString;
|
|
32
|
+
export declare function compiledScriptModuleTag(sourceFile: string): HtmlSafeString;
|
|
33
|
+
export declare function compiledScriptPreloadPaths(sourceFile: string): string[];
|
|
29
34
|
export declare function build(sourceDirectory: string, buildDirectory: string): Promise<AssetsManifest>;
|
|
30
|
-
export {};
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,8 @@ const DEFAULT_OPTIONS = {
|
|
|
14
14
|
let options = { ...DEFAULT_OPTIONS };
|
|
15
15
|
let esbuildContext = null;
|
|
16
16
|
let esbuildServer = null;
|
|
17
|
+
let splitEsbuildContext = null;
|
|
18
|
+
let splitEsbuildServer = null;
|
|
17
19
|
let relativeSourcePaths = null;
|
|
18
20
|
export async function init(newOptions) {
|
|
19
21
|
options = {
|
|
@@ -49,6 +51,22 @@ export async function init(newOptions) {
|
|
|
49
51
|
entryNames: '[dir]/[name]',
|
|
50
52
|
});
|
|
51
53
|
esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });
|
|
54
|
+
const splitSourceGlob = path.join(options.sourceDirectory, 'scripts', 'esm-bundles', '**', '*.{js,ts,jsx,tsx}');
|
|
55
|
+
const splitSourcePaths = await globby(splitSourceGlob);
|
|
56
|
+
relativeSourcePaths.push(...splitSourcePaths.map((p) => path.relative(options.sourceDirectory, p)));
|
|
57
|
+
splitEsbuildContext = await esbuild.context({
|
|
58
|
+
entryPoints: splitSourcePaths,
|
|
59
|
+
target: 'es2017',
|
|
60
|
+
format: 'esm',
|
|
61
|
+
sourcemap: 'inline',
|
|
62
|
+
bundle: true,
|
|
63
|
+
splitting: true,
|
|
64
|
+
write: false,
|
|
65
|
+
outbase: options.sourceDirectory,
|
|
66
|
+
outdir: options.buildDirectory,
|
|
67
|
+
entryNames: '[dir]/[name]',
|
|
68
|
+
});
|
|
69
|
+
splitEsbuildServer = await splitEsbuildContext.serve({ host: '0.0.0.0' });
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
/**
|
|
@@ -56,6 +74,7 @@ export async function init(newOptions) {
|
|
|
56
74
|
*/
|
|
57
75
|
export async function close() {
|
|
58
76
|
esbuildContext?.dispose();
|
|
77
|
+
splitEsbuildContext?.dispose();
|
|
59
78
|
}
|
|
60
79
|
export function assertConfigured() {
|
|
61
80
|
if (!options) {
|
|
@@ -78,13 +97,17 @@ export function handler() {
|
|
|
78
97
|
},
|
|
79
98
|
});
|
|
80
99
|
}
|
|
81
|
-
if (!esbuildServer) {
|
|
100
|
+
if (!esbuildServer || !splitEsbuildServer) {
|
|
82
101
|
throw new Error('esbuild server not initialized');
|
|
83
102
|
}
|
|
84
103
|
const { port } = esbuildServer;
|
|
104
|
+
const { port: splitPort } = splitEsbuildServer;
|
|
85
105
|
// We're running in dev mode, so we need to boot up esbuild to start building
|
|
86
106
|
// and watching our assets.
|
|
87
107
|
return function (req, res) {
|
|
108
|
+
const isSplitBundle = req.url?.startsWith('/scripts/esm-bundles') ||
|
|
109
|
+
// Chunked assets must be served by the split server.
|
|
110
|
+
req.url?.startsWith('/chunk-');
|
|
88
111
|
// esbuild will reject requests that come from hosts other than the host on
|
|
89
112
|
// which the esbuild dev server is listening:
|
|
90
113
|
// https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d
|
|
@@ -99,7 +122,7 @@ export function handler() {
|
|
|
99
122
|
delete headers['referer'];
|
|
100
123
|
const proxyReq = http.request({
|
|
101
124
|
hostname: '127.0.0.1',
|
|
102
|
-
port,
|
|
125
|
+
port: isSplitBundle ? splitPort : port,
|
|
103
126
|
path: req.url,
|
|
104
127
|
method: req.method,
|
|
105
128
|
headers,
|
|
@@ -133,11 +156,11 @@ function compiledPath(type, sourceFile) {
|
|
|
133
156
|
return options.publicPath + sourceFilePath.replace(/\.(js|ts)x?$/, '.js');
|
|
134
157
|
}
|
|
135
158
|
const manifest = readManifest();
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
159
|
+
const asset = manifest[sourceFilePath];
|
|
160
|
+
if (!asset) {
|
|
138
161
|
throw new Error(`Unknown ${type} asset: ${sourceFile}`);
|
|
139
162
|
}
|
|
140
|
-
return options.publicPath + assetPath;
|
|
163
|
+
return options.publicPath + asset.assetPath;
|
|
141
164
|
}
|
|
142
165
|
export function compiledScriptPath(sourceFile) {
|
|
143
166
|
return compiledPath('scripts', sourceFile);
|
|
@@ -146,14 +169,33 @@ export function compiledStylesheetPath(sourceFile) {
|
|
|
146
169
|
return compiledPath('stylesheets', sourceFile);
|
|
147
170
|
}
|
|
148
171
|
export function compiledScriptTag(sourceFile) {
|
|
172
|
+
// Creates a script tag for an IIFE bundle.
|
|
149
173
|
return html `<script src="${compiledScriptPath(sourceFile)}"></script>`;
|
|
150
174
|
}
|
|
151
175
|
export function compiledStylesheetTag(sourceFile) {
|
|
152
176
|
return html `<link rel="stylesheet" href="${compiledStylesheetPath(sourceFile)}" />`;
|
|
153
177
|
}
|
|
178
|
+
export function compiledScriptModuleTag(sourceFile) {
|
|
179
|
+
// Creates a module script tag for an ESM bundle.
|
|
180
|
+
return html `<script type="module" src="${compiledScriptPath(sourceFile)}"></script>`;
|
|
181
|
+
}
|
|
182
|
+
export function compiledScriptPreloadPaths(sourceFile) {
|
|
183
|
+
assertConfigured();
|
|
184
|
+
// In dev mode, we don't have a manifest, so we can't preload anything.
|
|
185
|
+
if (options.dev)
|
|
186
|
+
return [];
|
|
187
|
+
const manifest = readManifest();
|
|
188
|
+
const asset = manifest[`scripts/${sourceFile}`];
|
|
189
|
+
if (!asset) {
|
|
190
|
+
throw new Error(`Unknown script asset: ${sourceFile}`);
|
|
191
|
+
}
|
|
192
|
+
return asset.preloads.map((preload) => options.publicPath + preload);
|
|
193
|
+
}
|
|
154
194
|
async function buildAssets(sourceDirectory, buildDirectory) {
|
|
155
195
|
await fs.ensureDir(buildDirectory);
|
|
156
|
-
const
|
|
196
|
+
const scriptFiles = await globby(path.join(sourceDirectory, 'scripts', '*.{js,jsx,ts,tsx}'));
|
|
197
|
+
const styleFiles = await globby(path.join(sourceDirectory, 'stylesheets', '*.css'));
|
|
198
|
+
const files = [...scriptFiles, ...styleFiles];
|
|
157
199
|
const buildResult = await esbuild.build({
|
|
158
200
|
entryPoints: files,
|
|
159
201
|
target: 'es2017',
|
|
@@ -168,18 +210,60 @@ async function buildAssets(sourceDirectory, buildDirectory) {
|
|
|
168
210
|
entryNames: '[dir]/[name]-[hash]',
|
|
169
211
|
outbase: sourceDirectory,
|
|
170
212
|
outdir: buildDirectory,
|
|
213
|
+
metafile: true, // Write metadata about the build
|
|
214
|
+
});
|
|
215
|
+
// For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)
|
|
216
|
+
// Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,
|
|
217
|
+
// which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.
|
|
218
|
+
const scriptBundleFiles = await globby(path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'));
|
|
219
|
+
const esmBundleBuildResult = await esbuild.build({
|
|
220
|
+
entryPoints: scriptBundleFiles,
|
|
221
|
+
target: 'es2017',
|
|
222
|
+
format: 'esm',
|
|
223
|
+
sourcemap: 'linked',
|
|
224
|
+
bundle: true,
|
|
225
|
+
splitting: true,
|
|
226
|
+
minify: true,
|
|
227
|
+
entryNames: '[dir]/[name]-[hash]',
|
|
228
|
+
outbase: sourceDirectory,
|
|
229
|
+
outdir: buildDirectory,
|
|
171
230
|
metafile: true,
|
|
172
231
|
});
|
|
173
|
-
|
|
232
|
+
// Merge the resulting metafiles.
|
|
233
|
+
const metafile = {
|
|
234
|
+
inputs: { ...buildResult.metafile.inputs, ...esmBundleBuildResult.metafile.inputs },
|
|
235
|
+
outputs: { ...buildResult.metafile.outputs, ...esmBundleBuildResult.metafile.outputs },
|
|
236
|
+
};
|
|
237
|
+
return metafile;
|
|
174
238
|
}
|
|
175
239
|
function makeManifest(metafile, sourceDirectory, buildDirectory) {
|
|
176
240
|
const manifest = {};
|
|
177
241
|
Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
|
|
178
242
|
if (!meta.entryPoint)
|
|
179
243
|
return;
|
|
244
|
+
// Compute all the necessary preloads for each entrypoint. This includes
|
|
245
|
+
// any code-split chunks, as well as any files that are dynamically imported.
|
|
246
|
+
const preloads = new Set();
|
|
247
|
+
// Recursively walk the `imports` dependency tree
|
|
248
|
+
const visit = (entry) => {
|
|
249
|
+
if (!['import-statement', 'dynamic-import'].includes(entry.kind))
|
|
250
|
+
return;
|
|
251
|
+
if (preloads.has(entry.path))
|
|
252
|
+
return;
|
|
253
|
+
preloads.add(entry.path);
|
|
254
|
+
for (const imp of metafile.inputs[entry.path]?.imports ?? []) {
|
|
255
|
+
visit(imp);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
for (const imp of meta.imports) {
|
|
259
|
+
visit(imp);
|
|
260
|
+
}
|
|
180
261
|
const entryPath = path.relative(sourceDirectory, meta.entryPoint);
|
|
181
262
|
const assetPath = path.relative(buildDirectory, outputPath);
|
|
182
|
-
manifest[entryPath] =
|
|
263
|
+
manifest[entryPath] = {
|
|
264
|
+
assetPath,
|
|
265
|
+
preloads: [...preloads],
|
|
266
|
+
};
|
|
183
267
|
});
|
|
184
268
|
return manifest;
|
|
185
269
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import http, { type IncomingMessage, type ServerResponse } from 'node:http';\nimport path from 'path';\n\nimport esbuild, { type Metafile } from 'esbuild';\nimport expressStaticGzip from 'express-static-gzip';\nimport fs from 'fs-extra';\nimport { globby } from 'globby';\n\nimport { type HtmlSafeString, html } from '@prairielearn/html';\n\nconst DEFAULT_OPTIONS = {\n dev: process.env.NODE_ENV !== 'production',\n sourceDirectory: './assets',\n buildDirectory: './public/build',\n publicPath: '/build/',\n};\n\ntype AssetsManifest = Record<string, string>;\n\nexport interface CompiledAssetsOptions {\n /**\n * Whether the app is running in dev mode. If dev mode is enabled, then\n * assets will be built on the fly as they're requested. Otherwise, assets\n * should have been pre-compiled to the `buildDirectory` directory.\n */\n dev?: boolean;\n /** Root directory of assets. */\n sourceDirectory?: string;\n /** Directory where the built assets will be output to. */\n buildDirectory?: string;\n /** The path that assets will be served from, e.g. `/build/`. */\n publicPath?: string;\n}\n\nlet options: Required<CompiledAssetsOptions> = { ...DEFAULT_OPTIONS };\nlet esbuildContext: esbuild.BuildContext | null = null;\nlet esbuildServer: esbuild.ServeResult | null = null;\nlet relativeSourcePaths: string[] | null = null;\n\nexport async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<void> {\n options = {\n ...DEFAULT_OPTIONS,\n ...newOptions,\n };\n\n if (!options.publicPath.endsWith('/')) {\n options.publicPath += '/';\n }\n\n if (options.dev) {\n // Use esbuild's asset server in development.\n //\n // Note that esbuild doesn't support globs, so the server will not pick up\n // new entrypoints that are added while the server is running.\n const sourceGlob = path.join(options.sourceDirectory, '*', '*.{js,ts,jsx,tsx,css}');\n const sourcePaths = await globby(sourceGlob);\n\n // Save the result of globbing for the source paths so that we can later\n // check if a given filename exists.\n relativeSourcePaths = sourcePaths.map((p) => path.relative(options.sourceDirectory, p));\n\n esbuildContext = await esbuild.context({\n entryPoints: sourcePaths,\n target: 'es2017',\n format: 'iife',\n sourcemap: 'inline',\n bundle: true,\n write: false,\n loader: {\n '.woff': 'file',\n '.woff2': 'file',\n },\n outbase: options.sourceDirectory,\n outdir: options.buildDirectory,\n entryNames: '[dir]/[name]',\n });\n\n esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });\n }\n}\n\n/**\n * Shuts down the development assets compiler if it is running.\n */\nexport async function close() {\n esbuildContext?.dispose();\n}\n\nexport function assertConfigured(): void {\n if (!options) {\n throw new Error('@prairielearn/compiled-assets was not configured');\n }\n}\n\nexport function handler() {\n assertConfigured();\n\n if (!options?.dev) {\n // We're running in production: serve all assets from the build directory.\n // Set headers to cache for as long as possible, since the assets will\n // include content hashes in their filenames.\n return expressStaticGzip(options?.buildDirectory, {\n enableBrotli: true,\n // Prefer Brotli if the client supports it.\n orderPreference: ['br'],\n serveStatic: {\n maxAge: '31557600',\n immutable: true,\n },\n });\n }\n\n if (!esbuildServer) {\n throw new Error('esbuild server not initialized');\n }\n\n const { port } = esbuildServer;\n\n // We're running in dev mode, so we need to boot up esbuild to start building\n // and watching our assets.\n return function (req: IncomingMessage, res: ServerResponse) {\n // esbuild will reject requests that come from hosts other than the host on\n // which the esbuild dev server is listening:\n // https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d\n // https://github.com/evanw/esbuild/releases/tag/v0.25.0\n // We work around this by modifying the request headers to make it look like\n // the request is coming from localhost, which esbuild won't reject.\n const headers = structuredClone(req.headers);\n headers.host = 'localhost';\n delete headers['x-forwarded-for'];\n delete headers['x-forwarded-host'];\n delete headers['x-forwarded-proto'];\n delete headers['referer'];\n\n const proxyReq = http.request(\n {\n hostname: '127.0.0.1',\n port,\n path: req.url,\n method: req.method,\n headers,\n },\n (proxyRes) => {\n res.writeHead(proxyRes.statusCode ?? 500, proxyRes.headers);\n proxyRes.pipe(res, { end: true });\n },\n );\n req.pipe(proxyReq, { end: true });\n };\n}\n\nlet cachedManifest: AssetsManifest | null = null;\nfunction readManifest(): AssetsManifest {\n assertConfigured();\n\n if (!cachedManifest) {\n const manifestPath = path.join(options.buildDirectory, 'manifest.json');\n cachedManifest = fs.readJSONSync(manifestPath) as AssetsManifest;\n }\n\n return cachedManifest;\n}\n\nfunction compiledPath(type: 'scripts' | 'stylesheets', sourceFile: string): string {\n assertConfigured();\n const sourceFilePath = `${type}/${sourceFile}`;\n\n if (options.dev) {\n // To ensure that errors that would be raised in production are also raised\n // in development, we'll check for the existence of the asset file on disk.\n // This mirrors the production check of the file in the manifest: if a file\n // exists on disk, it should be in the manifest.\n if (!relativeSourcePaths?.find((p) => p === sourceFilePath)) {\n throw new Error(`Unknown ${type} asset: ${sourceFile}`);\n }\n\n return options.publicPath + sourceFilePath.replace(/\\.(js|ts)x?$/, '.js');\n }\n\n const manifest = readManifest();\n const assetPath = manifest[sourceFilePath];\n if (!assetPath) {\n throw new Error(`Unknown ${type} asset: ${sourceFile}`);\n }\n\n return options.publicPath + assetPath;\n}\n\nexport function compiledScriptPath(sourceFile: string): string {\n return compiledPath('scripts', sourceFile);\n}\n\nexport function compiledStylesheetPath(sourceFile: string): string {\n return compiledPath('stylesheets', sourceFile);\n}\n\nexport function compiledScriptTag(sourceFile: string): HtmlSafeString {\n return html`<script src=\"${compiledScriptPath(sourceFile)}\"></script>`;\n}\n\nexport function compiledStylesheetTag(sourceFile: string): HtmlSafeString {\n return html`<link rel=\"stylesheet\" href=\"${compiledStylesheetPath(sourceFile)}\" />`;\n}\n\nasync function buildAssets(sourceDirectory: string, buildDirectory: string) {\n await fs.ensureDir(buildDirectory);\n\n const files = await globby(path.join(sourceDirectory, '*/*.{js,jsx,ts,tsx,css}'));\n const buildResult = await esbuild.build({\n entryPoints: files,\n target: 'es2017',\n format: 'iife',\n sourcemap: 'linked',\n bundle: true,\n minify: true,\n loader: {\n '.woff': 'file',\n '.woff2': 'file',\n },\n entryNames: '[dir]/[name]-[hash]',\n outbase: sourceDirectory,\n outdir: buildDirectory,\n metafile: true,\n });\n\n return buildResult.metafile;\n}\n\nfunction makeManifest(\n metafile: Metafile,\n sourceDirectory: string,\n buildDirectory: string,\n): Record<string, string> {\n const manifest: Record<string, string> = {};\n Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {\n if (!meta.entryPoint) return;\n\n const entryPath = path.relative(sourceDirectory, meta.entryPoint);\n const assetPath = path.relative(buildDirectory, outputPath);\n manifest[entryPath] = assetPath;\n });\n return manifest;\n}\n\nexport async function build(\n sourceDirectory: string,\n buildDirectory: string,\n): Promise<AssetsManifest> {\n // Remove existing assets to ensure that no stale assets are left behind.\n await fs.remove(buildDirectory);\n\n const metafile = await buildAssets(sourceDirectory, buildDirectory);\n const manifest = makeManifest(metafile, sourceDirectory, buildDirectory);\n const manifestPath = path.join(buildDirectory, 'manifest.json');\n await fs.writeJSON(manifestPath, manifest);\n\n return manifest;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import http, { type IncomingMessage, type ServerResponse } from 'node:http';\nimport path from 'path';\n\nimport esbuild, { type Metafile } from 'esbuild';\nimport expressStaticGzip from 'express-static-gzip';\nimport fs from 'fs-extra';\nimport { globby } from 'globby';\n\nimport { type HtmlSafeString, html } from '@prairielearn/html';\n\nconst DEFAULT_OPTIONS = {\n dev: process.env.NODE_ENV !== 'production',\n sourceDirectory: './assets',\n buildDirectory: './public/build',\n publicPath: '/build/',\n};\n\nexport type AssetsManifest = Record<\n string,\n {\n assetPath: string;\n preloads: string[];\n }\n>;\n\nexport interface CompiledAssetsOptions {\n /**\n * Whether the app is running in dev mode. If dev mode is enabled, then\n * assets will be built on the fly as they're requested. Otherwise, assets\n * should have been pre-compiled to the `buildDirectory` directory.\n */\n dev?: boolean;\n /** Root directory of assets. */\n sourceDirectory?: string;\n /** Directory where the built assets will be output to. */\n buildDirectory?: string;\n /** The path that assets will be served from, e.g. `/build/`. */\n publicPath?: string;\n}\n\nlet options: Required<CompiledAssetsOptions> = { ...DEFAULT_OPTIONS };\n\nlet esbuildContext: esbuild.BuildContext | null = null;\nlet esbuildServer: esbuild.ServeResult | null = null;\n\nlet splitEsbuildContext: esbuild.BuildContext | null = null;\nlet splitEsbuildServer: esbuild.ServeResult | null = null;\n\nlet relativeSourcePaths: string[] | null = null;\n\nexport async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<void> {\n options = {\n ...DEFAULT_OPTIONS,\n ...newOptions,\n };\n\n if (!options.publicPath.endsWith('/')) {\n options.publicPath += '/';\n }\n\n if (options.dev) {\n // Use esbuild's asset server in development.\n //\n // Note that esbuild doesn't support globs, so the server will not pick up\n // new entrypoints that are added while the server is running.\n const sourceGlob = path.join(options.sourceDirectory, '*', '*.{js,ts,jsx,tsx,css}');\n const sourcePaths = await globby(sourceGlob);\n\n // Save the result of globbing for the source paths so that we can later\n // check if a given filename exists.\n relativeSourcePaths = sourcePaths.map((p) => path.relative(options.sourceDirectory, p));\n\n esbuildContext = await esbuild.context({\n entryPoints: sourcePaths,\n target: 'es2017',\n format: 'iife',\n sourcemap: 'inline',\n bundle: true,\n write: false,\n loader: {\n '.woff': 'file',\n '.woff2': 'file',\n },\n outbase: options.sourceDirectory,\n outdir: options.buildDirectory,\n entryNames: '[dir]/[name]',\n });\n esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });\n\n const splitSourceGlob = path.join(\n options.sourceDirectory,\n 'scripts',\n 'esm-bundles',\n '**',\n '*.{js,ts,jsx,tsx}',\n );\n const splitSourcePaths = await globby(splitSourceGlob);\n\n relativeSourcePaths.push(\n ...splitSourcePaths.map((p) => path.relative(options.sourceDirectory, p)),\n );\n\n splitEsbuildContext = await esbuild.context({\n entryPoints: splitSourcePaths,\n target: 'es2017',\n format: 'esm',\n sourcemap: 'inline',\n bundle: true,\n splitting: true,\n write: false,\n outbase: options.sourceDirectory,\n outdir: options.buildDirectory,\n entryNames: '[dir]/[name]',\n });\n splitEsbuildServer = await splitEsbuildContext.serve({ host: '0.0.0.0' });\n }\n}\n\n/**\n * Shuts down the development assets compiler if it is running.\n */\nexport async function close() {\n esbuildContext?.dispose();\n splitEsbuildContext?.dispose();\n}\n\nexport function assertConfigured(): void {\n if (!options) {\n throw new Error('@prairielearn/compiled-assets was not configured');\n }\n}\n\nexport function handler() {\n assertConfigured();\n\n if (!options?.dev) {\n // We're running in production: serve all assets from the build directory.\n // Set headers to cache for as long as possible, since the assets will\n // include content hashes in their filenames.\n return expressStaticGzip(options?.buildDirectory, {\n enableBrotli: true,\n // Prefer Brotli if the client supports it.\n orderPreference: ['br'],\n serveStatic: {\n maxAge: '31557600',\n immutable: true,\n },\n });\n }\n\n if (!esbuildServer || !splitEsbuildServer) {\n throw new Error('esbuild server not initialized');\n }\n\n const { port } = esbuildServer;\n const { port: splitPort } = splitEsbuildServer;\n\n // We're running in dev mode, so we need to boot up esbuild to start building\n // and watching our assets.\n return function (req: IncomingMessage, res: ServerResponse) {\n const isSplitBundle =\n req.url?.startsWith('/scripts/esm-bundles') ||\n // Chunked assets must be served by the split server.\n req.url?.startsWith('/chunk-');\n\n // esbuild will reject requests that come from hosts other than the host on\n // which the esbuild dev server is listening:\n // https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d\n // https://github.com/evanw/esbuild/releases/tag/v0.25.0\n // We work around this by modifying the request headers to make it look like\n // the request is coming from localhost, which esbuild won't reject.\n const headers = structuredClone(req.headers);\n headers.host = 'localhost';\n delete headers['x-forwarded-for'];\n delete headers['x-forwarded-host'];\n delete headers['x-forwarded-proto'];\n delete headers['referer'];\n\n const proxyReq = http.request(\n {\n hostname: '127.0.0.1',\n port: isSplitBundle ? splitPort : port,\n path: req.url,\n method: req.method,\n headers,\n },\n (proxyRes) => {\n res.writeHead(proxyRes.statusCode ?? 500, proxyRes.headers);\n proxyRes.pipe(res, { end: true });\n },\n );\n req.pipe(proxyReq, { end: true });\n };\n}\n\nlet cachedManifest: AssetsManifest | null = null;\nfunction readManifest(): AssetsManifest {\n assertConfigured();\n\n if (!cachedManifest) {\n const manifestPath = path.join(options.buildDirectory, 'manifest.json');\n cachedManifest = fs.readJSONSync(manifestPath) as AssetsManifest;\n }\n\n return cachedManifest;\n}\n\nfunction compiledPath(type: 'scripts' | 'stylesheets', sourceFile: string): string {\n assertConfigured();\n const sourceFilePath = `${type}/${sourceFile}`;\n\n if (options.dev) {\n // To ensure that errors that would be raised in production are also raised\n // in development, we'll check for the existence of the asset file on disk.\n // This mirrors the production check of the file in the manifest: if a file\n // exists on disk, it should be in the manifest.\n if (!relativeSourcePaths?.find((p) => p === sourceFilePath)) {\n throw new Error(`Unknown ${type} asset: ${sourceFile}`);\n }\n\n return options.publicPath + sourceFilePath.replace(/\\.(js|ts)x?$/, '.js');\n }\n\n const manifest = readManifest();\n const asset = manifest[sourceFilePath];\n if (!asset) {\n throw new Error(`Unknown ${type} asset: ${sourceFile}`);\n }\n\n return options.publicPath + asset.assetPath;\n}\n\nexport function compiledScriptPath(sourceFile: string): string {\n return compiledPath('scripts', sourceFile);\n}\n\nexport function compiledStylesheetPath(sourceFile: string): string {\n return compiledPath('stylesheets', sourceFile);\n}\n\nexport function compiledScriptTag(sourceFile: string): HtmlSafeString {\n // Creates a script tag for an IIFE bundle.\n return html`<script src=\"${compiledScriptPath(sourceFile)}\"></script>`;\n}\n\nexport function compiledStylesheetTag(sourceFile: string): HtmlSafeString {\n return html`<link rel=\"stylesheet\" href=\"${compiledStylesheetPath(sourceFile)}\" />`;\n}\n\nexport function compiledScriptModuleTag(sourceFile: string): HtmlSafeString {\n // Creates a module script tag for an ESM bundle.\n return html`<script type=\"module\" src=\"${compiledScriptPath(sourceFile)}\"></script>`;\n}\n\nexport function compiledScriptPreloadPaths(sourceFile: string): string[] {\n assertConfigured();\n\n // In dev mode, we don't have a manifest, so we can't preload anything.\n if (options.dev) return [];\n\n const manifest = readManifest();\n const asset = manifest[`scripts/${sourceFile}`];\n if (!asset) {\n throw new Error(`Unknown script asset: ${sourceFile}`);\n }\n\n return asset.preloads.map((preload) => options.publicPath + preload);\n}\n\nasync function buildAssets(sourceDirectory: string, buildDirectory: string): Promise<Metafile> {\n await fs.ensureDir(buildDirectory);\n\n const scriptFiles = await globby(path.join(sourceDirectory, 'scripts', '*.{js,jsx,ts,tsx}'));\n const styleFiles = await globby(path.join(sourceDirectory, 'stylesheets', '*.css'));\n const files = [...scriptFiles, ...styleFiles];\n const buildResult = await esbuild.build({\n entryPoints: files,\n target: 'es2017',\n format: 'iife',\n sourcemap: 'linked',\n bundle: true,\n minify: true,\n loader: {\n '.woff': 'file',\n '.woff2': 'file',\n },\n entryNames: '[dir]/[name]-[hash]',\n outbase: sourceDirectory,\n outdir: buildDirectory,\n metafile: true, // Write metadata about the build\n });\n\n // For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)\n // Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,\n // which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.\n const scriptBundleFiles = await globby(\n path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'),\n );\n const esmBundleBuildResult = await esbuild.build({\n entryPoints: scriptBundleFiles,\n target: 'es2017',\n format: 'esm',\n sourcemap: 'linked',\n bundle: true,\n splitting: true,\n minify: true,\n entryNames: '[dir]/[name]-[hash]',\n outbase: sourceDirectory,\n outdir: buildDirectory,\n metafile: true,\n });\n\n // Merge the resulting metafiles.\n const metafile: Metafile = {\n inputs: { ...buildResult.metafile.inputs, ...esmBundleBuildResult.metafile.inputs },\n outputs: { ...buildResult.metafile.outputs, ...esmBundleBuildResult.metafile.outputs },\n };\n\n return metafile;\n}\n\nfunction makeManifest(\n metafile: Metafile,\n sourceDirectory: string,\n buildDirectory: string,\n): AssetsManifest {\n const manifest: AssetsManifest = {};\n\n Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {\n if (!meta.entryPoint) return;\n\n // Compute all the necessary preloads for each entrypoint. This includes\n // any code-split chunks, as well as any files that are dynamically imported.\n const preloads = new Set<string>();\n\n // Recursively walk the `imports` dependency tree\n const visit = (entry: (typeof meta)['imports'][number]) => {\n if (!['import-statement', 'dynamic-import'].includes(entry.kind)) return;\n if (preloads.has(entry.path)) return;\n preloads.add(entry.path);\n for (const imp of metafile.inputs[entry.path]?.imports ?? []) {\n visit(imp);\n }\n };\n\n for (const imp of meta.imports) {\n visit(imp);\n }\n\n const entryPath = path.relative(sourceDirectory, meta.entryPoint);\n const assetPath = path.relative(buildDirectory, outputPath);\n manifest[entryPath] = {\n assetPath,\n preloads: [...preloads],\n };\n });\n\n return manifest;\n}\n\nexport async function build(\n sourceDirectory: string,\n buildDirectory: string,\n): Promise<AssetsManifest> {\n // Remove existing assets to ensure that no stale assets are left behind.\n await fs.remove(buildDirectory);\n\n const metafile = await buildAssets(sourceDirectory, buildDirectory);\n const manifest = makeManifest(metafile, sourceDirectory, buildDirectory);\n const manifestPath = path.join(buildDirectory, 'manifest.json');\n await fs.writeJSON(manifestPath, manifest);\n\n return manifest;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/compiled-assets",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@prairielearn/tsconfig": "^0.0.0",
|
|
29
|
-
"@types/node": "^
|
|
30
|
-
"@vitest/coverage-v8": "^3.2.
|
|
29
|
+
"@types/node": "^22.15.29",
|
|
30
|
+
"@vitest/coverage-v8": "^3.2.2",
|
|
31
31
|
"express": "^4.21.2",
|
|
32
32
|
"get-port": "^7.1.0",
|
|
33
33
|
"node-fetch": "^3.3.2",
|
|
34
34
|
"tsx": "^4.19.4",
|
|
35
35
|
"typescript": "^5.8.3",
|
|
36
|
-
"vitest": "^3.2.
|
|
36
|
+
"vitest": "^3.2.2"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/cli.ts
CHANGED
|
@@ -7,27 +7,36 @@ import { program } from 'commander';
|
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
8
|
import prettyBytes from 'pretty-bytes';
|
|
9
9
|
|
|
10
|
-
import { build } from './index.js';
|
|
10
|
+
import { type AssetsManifest, build } from './index.js';
|
|
11
11
|
|
|
12
12
|
const gzip = promisify(zlib.gzip);
|
|
13
13
|
const brotli = promisify(zlib.brotliCompress);
|
|
14
14
|
|
|
15
15
|
type CompressedSizes = Record<string, Record<string, number>>;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Writes gzip and brotli compressed versions of the assets in the specified directory.
|
|
19
|
+
* It reads each asset file, compresses it using gzip and brotli algorithms, and writes the compressed files
|
|
20
|
+
* with appropriate extensions (.gz and .br) in the same directory.
|
|
21
|
+
*
|
|
22
|
+
* @param destination Directory where the compressed assets will be written.
|
|
23
|
+
* @param manifest The assets manifest containing information about the assets.
|
|
24
|
+
* @returns A promise that resolves to an object containing the sizes of the original, gzip, and brotli compressed assets.
|
|
25
|
+
*/
|
|
17
26
|
async function writeCompressedAssets(
|
|
18
27
|
destination: string,
|
|
19
|
-
manifest:
|
|
28
|
+
manifest: AssetsManifest,
|
|
20
29
|
): Promise<CompressedSizes> {
|
|
21
30
|
const compressedSizes: CompressedSizes = {};
|
|
22
31
|
await Promise.all(
|
|
23
|
-
Object.values(manifest).map(async (
|
|
24
|
-
const destinationFilePath = path.resolve(destination,
|
|
32
|
+
Object.values(manifest).map(async (asset) => {
|
|
33
|
+
const destinationFilePath = path.resolve(destination, asset.assetPath);
|
|
25
34
|
const contents = await fs.readFile(destinationFilePath);
|
|
26
35
|
const gzipCompressed = await gzip(contents);
|
|
27
36
|
const brotliCompressed = await brotli(contents);
|
|
28
37
|
await fs.writeFile(`${destinationFilePath}.gz`, gzipCompressed);
|
|
29
38
|
await fs.writeFile(`${destinationFilePath}.br`, brotliCompressed);
|
|
30
|
-
compressedSizes[
|
|
39
|
+
compressedSizes[asset.assetPath] = {
|
|
31
40
|
raw: contents.length,
|
|
32
41
|
gzip: gzipCompressed.length,
|
|
33
42
|
brotli: brotliCompressed.length,
|
|
@@ -46,10 +55,10 @@ program.command('build <source> <destination>').action(async (source, destinatio
|
|
|
46
55
|
|
|
47
56
|
// Format the output into an object that we can pass to `console.table`.
|
|
48
57
|
const results: Record<string, any> = {};
|
|
49
|
-
Object.entries(manifest).forEach(([entryPoint,
|
|
50
|
-
const sizes = compressedSizes[assetPath];
|
|
58
|
+
Object.entries(manifest).forEach(([entryPoint, asset]) => {
|
|
59
|
+
const sizes = compressedSizes[asset.assetPath];
|
|
51
60
|
results[entryPoint] = {
|
|
52
|
-
'Output file': assetPath,
|
|
61
|
+
'Output file': asset.assetPath,
|
|
53
62
|
Size: prettyBytes(sizes.raw),
|
|
54
63
|
'Size (gzip)': prettyBytes(sizes.gzip),
|
|
55
64
|
'Size (brotli)': prettyBytes(sizes.brotli),
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,13 @@ const DEFAULT_OPTIONS = {
|
|
|
15
15
|
publicPath: '/build/',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
type AssetsManifest = Record<
|
|
18
|
+
export type AssetsManifest = Record<
|
|
19
|
+
string,
|
|
20
|
+
{
|
|
21
|
+
assetPath: string;
|
|
22
|
+
preloads: string[];
|
|
23
|
+
}
|
|
24
|
+
>;
|
|
19
25
|
|
|
20
26
|
export interface CompiledAssetsOptions {
|
|
21
27
|
/**
|
|
@@ -33,8 +39,13 @@ export interface CompiledAssetsOptions {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
let options: Required<CompiledAssetsOptions> = { ...DEFAULT_OPTIONS };
|
|
42
|
+
|
|
36
43
|
let esbuildContext: esbuild.BuildContext | null = null;
|
|
37
44
|
let esbuildServer: esbuild.ServeResult | null = null;
|
|
45
|
+
|
|
46
|
+
let splitEsbuildContext: esbuild.BuildContext | null = null;
|
|
47
|
+
let splitEsbuildServer: esbuild.ServeResult | null = null;
|
|
48
|
+
|
|
38
49
|
let relativeSourcePaths: string[] | null = null;
|
|
39
50
|
|
|
40
51
|
export async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<void> {
|
|
@@ -74,8 +85,34 @@ export async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<
|
|
|
74
85
|
outdir: options.buildDirectory,
|
|
75
86
|
entryNames: '[dir]/[name]',
|
|
76
87
|
});
|
|
77
|
-
|
|
78
88
|
esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });
|
|
89
|
+
|
|
90
|
+
const splitSourceGlob = path.join(
|
|
91
|
+
options.sourceDirectory,
|
|
92
|
+
'scripts',
|
|
93
|
+
'esm-bundles',
|
|
94
|
+
'**',
|
|
95
|
+
'*.{js,ts,jsx,tsx}',
|
|
96
|
+
);
|
|
97
|
+
const splitSourcePaths = await globby(splitSourceGlob);
|
|
98
|
+
|
|
99
|
+
relativeSourcePaths.push(
|
|
100
|
+
...splitSourcePaths.map((p) => path.relative(options.sourceDirectory, p)),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
splitEsbuildContext = await esbuild.context({
|
|
104
|
+
entryPoints: splitSourcePaths,
|
|
105
|
+
target: 'es2017',
|
|
106
|
+
format: 'esm',
|
|
107
|
+
sourcemap: 'inline',
|
|
108
|
+
bundle: true,
|
|
109
|
+
splitting: true,
|
|
110
|
+
write: false,
|
|
111
|
+
outbase: options.sourceDirectory,
|
|
112
|
+
outdir: options.buildDirectory,
|
|
113
|
+
entryNames: '[dir]/[name]',
|
|
114
|
+
});
|
|
115
|
+
splitEsbuildServer = await splitEsbuildContext.serve({ host: '0.0.0.0' });
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -84,6 +121,7 @@ export async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<
|
|
|
84
121
|
*/
|
|
85
122
|
export async function close() {
|
|
86
123
|
esbuildContext?.dispose();
|
|
124
|
+
splitEsbuildContext?.dispose();
|
|
87
125
|
}
|
|
88
126
|
|
|
89
127
|
export function assertConfigured(): void {
|
|
@@ -110,15 +148,21 @@ export function handler() {
|
|
|
110
148
|
});
|
|
111
149
|
}
|
|
112
150
|
|
|
113
|
-
if (!esbuildServer) {
|
|
151
|
+
if (!esbuildServer || !splitEsbuildServer) {
|
|
114
152
|
throw new Error('esbuild server not initialized');
|
|
115
153
|
}
|
|
116
154
|
|
|
117
155
|
const { port } = esbuildServer;
|
|
156
|
+
const { port: splitPort } = splitEsbuildServer;
|
|
118
157
|
|
|
119
158
|
// We're running in dev mode, so we need to boot up esbuild to start building
|
|
120
159
|
// and watching our assets.
|
|
121
160
|
return function (req: IncomingMessage, res: ServerResponse) {
|
|
161
|
+
const isSplitBundle =
|
|
162
|
+
req.url?.startsWith('/scripts/esm-bundles') ||
|
|
163
|
+
// Chunked assets must be served by the split server.
|
|
164
|
+
req.url?.startsWith('/chunk-');
|
|
165
|
+
|
|
122
166
|
// esbuild will reject requests that come from hosts other than the host on
|
|
123
167
|
// which the esbuild dev server is listening:
|
|
124
168
|
// https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d
|
|
@@ -135,7 +179,7 @@ export function handler() {
|
|
|
135
179
|
const proxyReq = http.request(
|
|
136
180
|
{
|
|
137
181
|
hostname: '127.0.0.1',
|
|
138
|
-
port,
|
|
182
|
+
port: isSplitBundle ? splitPort : port,
|
|
139
183
|
path: req.url,
|
|
140
184
|
method: req.method,
|
|
141
185
|
headers,
|
|
@@ -178,12 +222,12 @@ function compiledPath(type: 'scripts' | 'stylesheets', sourceFile: string): stri
|
|
|
178
222
|
}
|
|
179
223
|
|
|
180
224
|
const manifest = readManifest();
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
225
|
+
const asset = manifest[sourceFilePath];
|
|
226
|
+
if (!asset) {
|
|
183
227
|
throw new Error(`Unknown ${type} asset: ${sourceFile}`);
|
|
184
228
|
}
|
|
185
229
|
|
|
186
|
-
return options.publicPath + assetPath;
|
|
230
|
+
return options.publicPath + asset.assetPath;
|
|
187
231
|
}
|
|
188
232
|
|
|
189
233
|
export function compiledScriptPath(sourceFile: string): string {
|
|
@@ -195,6 +239,7 @@ export function compiledStylesheetPath(sourceFile: string): string {
|
|
|
195
239
|
}
|
|
196
240
|
|
|
197
241
|
export function compiledScriptTag(sourceFile: string): HtmlSafeString {
|
|
242
|
+
// Creates a script tag for an IIFE bundle.
|
|
198
243
|
return html`<script src="${compiledScriptPath(sourceFile)}"></script>`;
|
|
199
244
|
}
|
|
200
245
|
|
|
@@ -202,10 +247,32 @@ export function compiledStylesheetTag(sourceFile: string): HtmlSafeString {
|
|
|
202
247
|
return html`<link rel="stylesheet" href="${compiledStylesheetPath(sourceFile)}" />`;
|
|
203
248
|
}
|
|
204
249
|
|
|
205
|
-
|
|
250
|
+
export function compiledScriptModuleTag(sourceFile: string): HtmlSafeString {
|
|
251
|
+
// Creates a module script tag for an ESM bundle.
|
|
252
|
+
return html`<script type="module" src="${compiledScriptPath(sourceFile)}"></script>`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function compiledScriptPreloadPaths(sourceFile: string): string[] {
|
|
256
|
+
assertConfigured();
|
|
257
|
+
|
|
258
|
+
// In dev mode, we don't have a manifest, so we can't preload anything.
|
|
259
|
+
if (options.dev) return [];
|
|
260
|
+
|
|
261
|
+
const manifest = readManifest();
|
|
262
|
+
const asset = manifest[`scripts/${sourceFile}`];
|
|
263
|
+
if (!asset) {
|
|
264
|
+
throw new Error(`Unknown script asset: ${sourceFile}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return asset.preloads.map((preload) => options.publicPath + preload);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function buildAssets(sourceDirectory: string, buildDirectory: string): Promise<Metafile> {
|
|
206
271
|
await fs.ensureDir(buildDirectory);
|
|
207
272
|
|
|
208
|
-
const
|
|
273
|
+
const scriptFiles = await globby(path.join(sourceDirectory, 'scripts', '*.{js,jsx,ts,tsx}'));
|
|
274
|
+
const styleFiles = await globby(path.join(sourceDirectory, 'stylesheets', '*.css'));
|
|
275
|
+
const files = [...scriptFiles, ...styleFiles];
|
|
209
276
|
const buildResult = await esbuild.build({
|
|
210
277
|
entryPoints: files,
|
|
211
278
|
target: 'es2017',
|
|
@@ -220,25 +287,74 @@ async function buildAssets(sourceDirectory: string, buildDirectory: string) {
|
|
|
220
287
|
entryNames: '[dir]/[name]-[hash]',
|
|
221
288
|
outbase: sourceDirectory,
|
|
222
289
|
outdir: buildDirectory,
|
|
290
|
+
metafile: true, // Write metadata about the build
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)
|
|
294
|
+
// Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,
|
|
295
|
+
// which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.
|
|
296
|
+
const scriptBundleFiles = await globby(
|
|
297
|
+
path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'),
|
|
298
|
+
);
|
|
299
|
+
const esmBundleBuildResult = await esbuild.build({
|
|
300
|
+
entryPoints: scriptBundleFiles,
|
|
301
|
+
target: 'es2017',
|
|
302
|
+
format: 'esm',
|
|
303
|
+
sourcemap: 'linked',
|
|
304
|
+
bundle: true,
|
|
305
|
+
splitting: true,
|
|
306
|
+
minify: true,
|
|
307
|
+
entryNames: '[dir]/[name]-[hash]',
|
|
308
|
+
outbase: sourceDirectory,
|
|
309
|
+
outdir: buildDirectory,
|
|
223
310
|
metafile: true,
|
|
224
311
|
});
|
|
225
312
|
|
|
226
|
-
|
|
313
|
+
// Merge the resulting metafiles.
|
|
314
|
+
const metafile: Metafile = {
|
|
315
|
+
inputs: { ...buildResult.metafile.inputs, ...esmBundleBuildResult.metafile.inputs },
|
|
316
|
+
outputs: { ...buildResult.metafile.outputs, ...esmBundleBuildResult.metafile.outputs },
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return metafile;
|
|
227
320
|
}
|
|
228
321
|
|
|
229
322
|
function makeManifest(
|
|
230
323
|
metafile: Metafile,
|
|
231
324
|
sourceDirectory: string,
|
|
232
325
|
buildDirectory: string,
|
|
233
|
-
):
|
|
234
|
-
const manifest:
|
|
326
|
+
): AssetsManifest {
|
|
327
|
+
const manifest: AssetsManifest = {};
|
|
328
|
+
|
|
235
329
|
Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
|
|
236
330
|
if (!meta.entryPoint) return;
|
|
237
331
|
|
|
332
|
+
// Compute all the necessary preloads for each entrypoint. This includes
|
|
333
|
+
// any code-split chunks, as well as any files that are dynamically imported.
|
|
334
|
+
const preloads = new Set<string>();
|
|
335
|
+
|
|
336
|
+
// Recursively walk the `imports` dependency tree
|
|
337
|
+
const visit = (entry: (typeof meta)['imports'][number]) => {
|
|
338
|
+
if (!['import-statement', 'dynamic-import'].includes(entry.kind)) return;
|
|
339
|
+
if (preloads.has(entry.path)) return;
|
|
340
|
+
preloads.add(entry.path);
|
|
341
|
+
for (const imp of metafile.inputs[entry.path]?.imports ?? []) {
|
|
342
|
+
visit(imp);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
for (const imp of meta.imports) {
|
|
347
|
+
visit(imp);
|
|
348
|
+
}
|
|
349
|
+
|
|
238
350
|
const entryPath = path.relative(sourceDirectory, meta.entryPoint);
|
|
239
351
|
const assetPath = path.relative(buildDirectory, outputPath);
|
|
240
|
-
manifest[entryPath] =
|
|
352
|
+
manifest[entryPath] = {
|
|
353
|
+
assetPath,
|
|
354
|
+
preloads: [...preloads],
|
|
355
|
+
};
|
|
241
356
|
});
|
|
357
|
+
|
|
242
358
|
return manifest;
|
|
243
359
|
}
|
|
244
360
|
|