@prairielearn/compiled-assets 3.0.18 → 3.2.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 +12 -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 +104 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/cli.ts +17 -8
- package/src/index.ts +141 -13
- package/.mocharc.cjs +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @prairielearn/compiled-assets
|
|
2
2
|
|
|
3
|
+
## 3.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6e2ba4f: Define `process.env.NODE_ENV` when bundling JavaScript
|
|
8
|
+
|
|
9
|
+
## 3.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 791b1c7: Support preloading assets, and assets processed with ESM + code splitting
|
|
14
|
+
|
|
3
15
|
## 3.0.18
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
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 = {
|
|
@@ -47,8 +49,30 @@ export async function init(newOptions) {
|
|
|
47
49
|
outbase: options.sourceDirectory,
|
|
48
50
|
outdir: options.buildDirectory,
|
|
49
51
|
entryNames: '[dir]/[name]',
|
|
52
|
+
define: {
|
|
53
|
+
'process.env.NODE_ENV': '"development"',
|
|
54
|
+
},
|
|
50
55
|
});
|
|
51
56
|
esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });
|
|
57
|
+
const splitSourceGlob = path.join(options.sourceDirectory, 'scripts', 'esm-bundles', '**', '*.{js,ts,jsx,tsx}');
|
|
58
|
+
const splitSourcePaths = await globby(splitSourceGlob);
|
|
59
|
+
relativeSourcePaths.push(...splitSourcePaths.map((p) => path.relative(options.sourceDirectory, p)));
|
|
60
|
+
splitEsbuildContext = await esbuild.context({
|
|
61
|
+
entryPoints: splitSourcePaths,
|
|
62
|
+
target: 'es2017',
|
|
63
|
+
format: 'esm',
|
|
64
|
+
sourcemap: 'inline',
|
|
65
|
+
bundle: true,
|
|
66
|
+
splitting: true,
|
|
67
|
+
write: false,
|
|
68
|
+
outbase: options.sourceDirectory,
|
|
69
|
+
outdir: options.buildDirectory,
|
|
70
|
+
entryNames: '[dir]/[name]',
|
|
71
|
+
define: {
|
|
72
|
+
'process.env.NODE_ENV': '"development"',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
splitEsbuildServer = await splitEsbuildContext.serve({ host: '0.0.0.0' });
|
|
52
76
|
}
|
|
53
77
|
}
|
|
54
78
|
/**
|
|
@@ -56,6 +80,7 @@ export async function init(newOptions) {
|
|
|
56
80
|
*/
|
|
57
81
|
export async function close() {
|
|
58
82
|
esbuildContext?.dispose();
|
|
83
|
+
splitEsbuildContext?.dispose();
|
|
59
84
|
}
|
|
60
85
|
export function assertConfigured() {
|
|
61
86
|
if (!options) {
|
|
@@ -78,13 +103,17 @@ export function handler() {
|
|
|
78
103
|
},
|
|
79
104
|
});
|
|
80
105
|
}
|
|
81
|
-
if (!esbuildServer) {
|
|
106
|
+
if (!esbuildServer || !splitEsbuildServer) {
|
|
82
107
|
throw new Error('esbuild server not initialized');
|
|
83
108
|
}
|
|
84
109
|
const { port } = esbuildServer;
|
|
110
|
+
const { port: splitPort } = splitEsbuildServer;
|
|
85
111
|
// We're running in dev mode, so we need to boot up esbuild to start building
|
|
86
112
|
// and watching our assets.
|
|
87
113
|
return function (req, res) {
|
|
114
|
+
const isSplitBundle = req.url?.startsWith('/scripts/esm-bundles') ||
|
|
115
|
+
// Chunked assets must be served by the split server.
|
|
116
|
+
req.url?.startsWith('/chunk-');
|
|
88
117
|
// esbuild will reject requests that come from hosts other than the host on
|
|
89
118
|
// which the esbuild dev server is listening:
|
|
90
119
|
// https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d
|
|
@@ -99,7 +128,7 @@ export function handler() {
|
|
|
99
128
|
delete headers['referer'];
|
|
100
129
|
const proxyReq = http.request({
|
|
101
130
|
hostname: '127.0.0.1',
|
|
102
|
-
port,
|
|
131
|
+
port: isSplitBundle ? splitPort : port,
|
|
103
132
|
path: req.url,
|
|
104
133
|
method: req.method,
|
|
105
134
|
headers,
|
|
@@ -133,11 +162,11 @@ function compiledPath(type, sourceFile) {
|
|
|
133
162
|
return options.publicPath + sourceFilePath.replace(/\.(js|ts)x?$/, '.js');
|
|
134
163
|
}
|
|
135
164
|
const manifest = readManifest();
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
165
|
+
const asset = manifest[sourceFilePath];
|
|
166
|
+
if (!asset) {
|
|
138
167
|
throw new Error(`Unknown ${type} asset: ${sourceFile}`);
|
|
139
168
|
}
|
|
140
|
-
return options.publicPath + assetPath;
|
|
169
|
+
return options.publicPath + asset.assetPath;
|
|
141
170
|
}
|
|
142
171
|
export function compiledScriptPath(sourceFile) {
|
|
143
172
|
return compiledPath('scripts', sourceFile);
|
|
@@ -146,14 +175,33 @@ export function compiledStylesheetPath(sourceFile) {
|
|
|
146
175
|
return compiledPath('stylesheets', sourceFile);
|
|
147
176
|
}
|
|
148
177
|
export function compiledScriptTag(sourceFile) {
|
|
178
|
+
// Creates a script tag for an IIFE bundle.
|
|
149
179
|
return html `<script src="${compiledScriptPath(sourceFile)}"></script>`;
|
|
150
180
|
}
|
|
151
181
|
export function compiledStylesheetTag(sourceFile) {
|
|
152
182
|
return html `<link rel="stylesheet" href="${compiledStylesheetPath(sourceFile)}" />`;
|
|
153
183
|
}
|
|
184
|
+
export function compiledScriptModuleTag(sourceFile) {
|
|
185
|
+
// Creates a module script tag for an ESM bundle.
|
|
186
|
+
return html `<script type="module" src="${compiledScriptPath(sourceFile)}"></script>`;
|
|
187
|
+
}
|
|
188
|
+
export function compiledScriptPreloadPaths(sourceFile) {
|
|
189
|
+
assertConfigured();
|
|
190
|
+
// In dev mode, we don't have a manifest, so we can't preload anything.
|
|
191
|
+
if (options.dev)
|
|
192
|
+
return [];
|
|
193
|
+
const manifest = readManifest();
|
|
194
|
+
const asset = manifest[`scripts/${sourceFile}`];
|
|
195
|
+
if (!asset) {
|
|
196
|
+
throw new Error(`Unknown script asset: ${sourceFile}`);
|
|
197
|
+
}
|
|
198
|
+
return asset.preloads.map((preload) => options.publicPath + preload);
|
|
199
|
+
}
|
|
154
200
|
async function buildAssets(sourceDirectory, buildDirectory) {
|
|
155
201
|
await fs.ensureDir(buildDirectory);
|
|
156
|
-
const
|
|
202
|
+
const scriptFiles = await globby(path.join(sourceDirectory, 'scripts', '*.{js,jsx,ts,tsx}'));
|
|
203
|
+
const styleFiles = await globby(path.join(sourceDirectory, 'stylesheets', '*.css'));
|
|
204
|
+
const files = [...scriptFiles, ...styleFiles];
|
|
157
205
|
const buildResult = await esbuild.build({
|
|
158
206
|
entryPoints: files,
|
|
159
207
|
target: 'es2017',
|
|
@@ -168,18 +216,66 @@ async function buildAssets(sourceDirectory, buildDirectory) {
|
|
|
168
216
|
entryNames: '[dir]/[name]-[hash]',
|
|
169
217
|
outbase: sourceDirectory,
|
|
170
218
|
outdir: buildDirectory,
|
|
219
|
+
define: {
|
|
220
|
+
'process.env.NODE_ENV': '"production"',
|
|
221
|
+
},
|
|
222
|
+
metafile: true, // Write metadata about the build
|
|
223
|
+
});
|
|
224
|
+
// For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)
|
|
225
|
+
// Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,
|
|
226
|
+
// which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.
|
|
227
|
+
const scriptBundleFiles = await globby(path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'));
|
|
228
|
+
const esmBundleBuildResult = await esbuild.build({
|
|
229
|
+
entryPoints: scriptBundleFiles,
|
|
230
|
+
target: 'es2017',
|
|
231
|
+
format: 'esm',
|
|
232
|
+
sourcemap: 'linked',
|
|
233
|
+
bundle: true,
|
|
234
|
+
splitting: true,
|
|
235
|
+
minify: true,
|
|
236
|
+
entryNames: '[dir]/[name]-[hash]',
|
|
237
|
+
outbase: sourceDirectory,
|
|
238
|
+
outdir: buildDirectory,
|
|
239
|
+
define: {
|
|
240
|
+
'process.env.NODE_ENV': '"production"',
|
|
241
|
+
},
|
|
171
242
|
metafile: true,
|
|
172
243
|
});
|
|
173
|
-
|
|
244
|
+
// Merge the resulting metafiles.
|
|
245
|
+
const metafile = {
|
|
246
|
+
inputs: { ...buildResult.metafile.inputs, ...esmBundleBuildResult.metafile.inputs },
|
|
247
|
+
outputs: { ...buildResult.metafile.outputs, ...esmBundleBuildResult.metafile.outputs },
|
|
248
|
+
};
|
|
249
|
+
return metafile;
|
|
174
250
|
}
|
|
175
251
|
function makeManifest(metafile, sourceDirectory, buildDirectory) {
|
|
176
252
|
const manifest = {};
|
|
177
253
|
Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
|
|
178
254
|
if (!meta.entryPoint)
|
|
179
255
|
return;
|
|
256
|
+
// Compute all the necessary preloads for each entrypoint. This includes
|
|
257
|
+
// any code-split chunks, as well as any files that are dynamically imported.
|
|
258
|
+
const preloads = new Set();
|
|
259
|
+
// Recursively walk the `imports` dependency tree
|
|
260
|
+
const visit = (entry) => {
|
|
261
|
+
if (!['import-statement', 'dynamic-import'].includes(entry.kind))
|
|
262
|
+
return;
|
|
263
|
+
if (preloads.has(entry.path))
|
|
264
|
+
return;
|
|
265
|
+
preloads.add(entry.path);
|
|
266
|
+
for (const imp of metafile.inputs[entry.path]?.imports ?? []) {
|
|
267
|
+
visit(imp);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
for (const imp of meta.imports) {
|
|
271
|
+
visit(imp);
|
|
272
|
+
}
|
|
180
273
|
const entryPath = path.relative(sourceDirectory, meta.entryPoint);
|
|
181
274
|
const assetPath = path.relative(buildDirectory, outputPath);
|
|
182
|
-
manifest[entryPath] =
|
|
275
|
+
manifest[entryPath] = {
|
|
276
|
+
assetPath,
|
|
277
|
+
preloads: [...preloads],
|
|
278
|
+
};
|
|
183
279
|
});
|
|
184
280
|
return manifest;
|
|
185
281
|
}
|
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":"AAAA,OAAO,IAAI,EAAE,EAA6C,MAAM,WAAW,CAAC;AAC5E,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,OAAO,EAAE,EAAiB,MAAM,SAAS,CAAC;AACjD,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAuB,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,eAAe,GAAG;IACtB,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;IAC1C,eAAe,EAAE,UAAU;IAC3B,cAAc,EAAE,gBAAgB;IAChC,UAAU,EAAE,SAAS;CACtB,CAAC;AAyBF,IAAI,OAAO,GAAoC,EAAE,GAAG,eAAe,EAAE,CAAC;AAEtE,IAAI,cAAc,GAAgC,IAAI,CAAC;AACvD,IAAI,aAAa,GAA+B,IAAI,CAAC;AAErD,IAAI,mBAAmB,GAAgC,IAAI,CAAC;AAC5D,IAAI,kBAAkB,GAA+B,IAAI,CAAC;AAE1D,IAAI,mBAAmB,GAAoB,IAAI,CAAC;AAEhD,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,UAA0C;IACnE,OAAO,GAAG;QACR,GAAG,eAAe;QAClB,GAAG,UAAU;KACd,CAAC;IAEF,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC;IAC5B,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,6CAA6C;QAC7C,EAAE;QACF,0EAA0E;QAC1E,8DAA8D;QAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC;QACpF,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;QAE7C,wEAAwE;QACxE,oCAAoC;QACpC,mBAAmB,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,CAAC;QAExF,cAAc,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC;YACrC,WAAW,EAAE,WAAW;YACxB,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,MAAM;YACd,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,IAAI;YACZ,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE;gBACN,OAAO,EAAE,MAAM;gBACf,QAAQ,EAAE,MAAM;aACjB;YACD,OAAO,EAAE,OAAO,CAAC,eAAe;YAChC,MAAM,EAAE,OAAO,CAAC,cAAc;YAC9B,UAAU,EAAE,cAAc;YAC1B,MAAM,EAAE;gBACN,sBAAsB,EAAE,eAAe;aACxC;SACF,CAAC,CAAC;QACH,aAAa,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QAEhE,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAC/B,OAAO,CAAC,eAAe,EACvB,SAAS,EACT,aAAa,EACb,IAAI,EACJ,mBAAmB,CACpB,CAAC;QACF,MAAM,gBAAgB,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;QAEvD,mBAAmB,CAAC,IAAI,CACtB,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,CAC1E,CAAC;QAEF,mBAAmB,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC;YAC1C,WAAW,EAAE,gBAAgB;YAC7B,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,KAAK;YACb,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE,IAAI;YACf,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,OAAO,CAAC,eAAe;YAChC,MAAM,EAAE,OAAO,CAAC,cAAc;YAC9B,UAAU,EAAE,cAAc;YAC1B,MAAM,EAAE;gBACN,sBAAsB,EAAE,eAAe;aACxC;SACF,CAAC,CAAC;QACH,kBAAkB,GAAG,MAAM,mBAAmB,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK;IACzB,cAAc,EAAE,OAAO,EAAE,CAAC;IAC1B,mBAAmB,EAAE,OAAO,EAAE,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,gBAAgB,EAAE,CAAC;IAEnB,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;QAClB,0EAA0E;QAC1E,sEAAsE;QACtE,6CAA6C;QAC7C,OAAO,iBAAiB,CAAC,OAAO,EAAE,cAAc,EAAE;YAChD,YAAY,EAAE,IAAI;YAClB,2CAA2C;YAC3C,eAAe,EAAE,CAAC,IAAI,CAAC;YACvB,WAAW,EAAE;gBACX,MAAM,EAAE,UAAU;gBAClB,SAAS,EAAE,IAAI;aAChB;SACF,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,aAAa,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC;IAC/B,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,kBAAkB,CAAC;IAE/C,6EAA6E;IAC7E,2BAA2B;IAC3B,OAAO,UAAU,GAAoB,EAAE,GAAmB;QACxD,MAAM,aAAa,GACjB,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,sBAAsB,CAAC;YAC3C,qDAAqD;YACrD,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;QAEjC,2EAA2E;QAC3E,6CAA6C;QAC7C,mFAAmF;QACnF,wDAAwD;QACxD,4EAA4E;QAC5E,oEAAoE;QACpE,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,GAAG,WAAW,CAAC;QAC3B,OAAO,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAClC,OAAO,OAAO,CAAC,kBAAkB,CAAC,CAAC;QACnC,OAAO,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACpC,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC;QAE1B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAC3B;YACE,QAAQ,EAAE,WAAW;YACrB,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;YACtC,IAAI,EAAE,GAAG,CAAC,GAAG;YACb,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,OAAO;SACR,EACD,CAAC,QAAQ,EAAE,EAAE;YACX,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,IAAI,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;YAC5D,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC,CACF,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC;AACJ,CAAC;AAED,IAAI,cAAc,GAA0B,IAAI,CAAC;AACjD,SAAS,YAAY;IACnB,gBAAgB,EAAE,CAAC;IAEnB,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;QACxE,cAAc,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,CAAmB,CAAC;IACnE,CAAC;IAED,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,SAAS,YAAY,CAAC,IAA+B,EAAE,UAAkB;IACvE,gBAAgB,EAAE,CAAC;IACnB,MAAM,cAAc,GAAG,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC;IAE/C,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,2EAA2E;QAC3E,2EAA2E;QAC3E,2EAA2E;QAC3E,gDAAgD;QAChD,IAAI,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,cAAc,CAAC,EAAE,CAAC;YAC5D,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,WAAW,UAAU,EAAE,CAAC,CAAC;QAC1D,CAAC;QAED,OAAO,OAAO,CAAC,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,WAAW,UAAU,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,OAAO,CAAC,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,UAAkB;IACnD,OAAO,YAAY,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,UAAkB;IACvD,OAAO,YAAY,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,UAAkB;IAClD,2CAA2C;IAC3C,OAAO,IAAI,CAAA,gBAAgB,kBAAkB,CAAC,UAAU,CAAC,aAAa,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,UAAkB;IACtD,OAAO,IAAI,CAAA,gCAAgC,sBAAsB,CAAC,UAAU,CAAC,MAAM,CAAC;AACtF,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,UAAkB;IACxD,iDAAiD;IACjD,OAAO,IAAI,CAAA,8BAA8B,kBAAkB,CAAC,UAAU,CAAC,aAAa,CAAC;AACvF,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,UAAkB;IAC3D,gBAAgB,EAAE,CAAC;IAEnB,uEAAuE;IACvE,IAAI,OAAO,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IAE3B,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,UAAU,EAAE,CAAC,CAAC;IAChD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,yBAAyB,UAAU,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,CAAC;AACvE,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,eAAuB,EAAE,cAAsB;IACxE,MAAM,EAAE,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAEnC,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,SAAS,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAC7F,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,CAAC,GAAG,WAAW,EAAE,GAAG,UAAU,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;QACtC,WAAW,EAAE,KAAK;QAClB,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,MAAM;QACd,SAAS,EAAE,QAAQ;QACnB,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE;YACN,OAAO,EAAE,MAAM;YACf,QAAQ,EAAE,MAAM;SACjB;QACD,UAAU,EAAE,qBAAqB;QACjC,OAAO,EAAE,eAAe;QACxB,MAAM,EAAE,cAAc;QACtB,MAAM,EAAE;YACN,sBAAsB,EAAE,cAAc;SACvC;QACD,QAAQ,EAAE,IAAI,EAAE,iCAAiC;KAClD,CAAC,CAAC;IAEH,qGAAqG;IACrG,qGAAqG;IACrG,yGAAyG;IACzG,MAAM,iBAAiB,GAAG,MAAM,MAAM,CACpC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAC7E,CAAC;IACF,MAAM,oBAAoB,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;QAC/C,WAAW,EAAE,iBAAiB;QAC9B,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,KAAK;QACb,SAAS,EAAE,QAAQ;QACnB,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,IAAI;QACZ,UAAU,EAAE,qBAAqB;QACjC,OAAO,EAAE,eAAe;QACxB,MAAM,EAAE,cAAc;QACtB,MAAM,EAAE;YACN,sBAAsB,EAAE,cAAc;SACvC;QACD,QAAQ,EAAE,IAAI;KACf,CAAC,CAAC;IAEH,iCAAiC;IACjC,MAAM,QAAQ,GAAa;QACzB,MAAM,EAAE,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,oBAAoB,CAAC,QAAQ,CAAC,MAAM,EAAE;QACnF,OAAO,EAAE,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,oBAAoB,CAAC,QAAQ,CAAC,OAAO,EAAE;KACvF,CAAC;IAEF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,YAAY,CACnB,QAAkB,EAClB,eAAuB,EACvB,cAAsB;IAEtB,MAAM,QAAQ,GAAmB,EAAE,CAAC;IAEpC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,EAAE;QAC9D,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO;QAE7B,wEAAwE;QACxE,6EAA6E;QAC7E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QAEnC,iDAAiD;QACjD,MAAM,KAAK,GAAG,CAAC,KAAuC,EAAE,EAAE;YACxD,IAAI,CAAC,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,OAAO;YACzE,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,OAAO;YACrC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACzB,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,IAAI,EAAE,EAAE,CAAC;gBAC7D,KAAK,CAAC,GAAG,CAAC,CAAC;YACb,CAAC;QACH,CAAC,CAAC;QAEF,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC/B,KAAK,CAAC,GAAG,CAAC,CAAC;QACb,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAClE,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;QAC5D,QAAQ,CAAC,SAAS,CAAC,GAAG;YACpB,SAAS;YACT,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC;SACxB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,eAAuB,EACvB,cAAsB;IAEtB,yEAAyE;IACzE,MAAM,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAEhC,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,eAAe,EAAE,cAAc,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,EAAE,eAAe,EAAE,cAAc,CAAC,CAAC;IACzE,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAChE,MAAM,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IAE3C,OAAO,QAAQ,CAAC;AAClB,CAAC","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 define: {\n 'process.env.NODE_ENV': '\"development\"',\n },\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 define: {\n 'process.env.NODE_ENV': '\"development\"',\n },\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 define: {\n 'process.env.NODE_ENV': '\"production\"',\n },\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 define: {\n 'process.env.NODE_ENV': '\"production\"',\n },\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.2.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> {
|
|
@@ -73,9 +84,41 @@ export async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<
|
|
|
73
84
|
outbase: options.sourceDirectory,
|
|
74
85
|
outdir: options.buildDirectory,
|
|
75
86
|
entryNames: '[dir]/[name]',
|
|
87
|
+
define: {
|
|
88
|
+
'process.env.NODE_ENV': '"development"',
|
|
89
|
+
},
|
|
76
90
|
});
|
|
77
|
-
|
|
78
91
|
esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });
|
|
92
|
+
|
|
93
|
+
const splitSourceGlob = path.join(
|
|
94
|
+
options.sourceDirectory,
|
|
95
|
+
'scripts',
|
|
96
|
+
'esm-bundles',
|
|
97
|
+
'**',
|
|
98
|
+
'*.{js,ts,jsx,tsx}',
|
|
99
|
+
);
|
|
100
|
+
const splitSourcePaths = await globby(splitSourceGlob);
|
|
101
|
+
|
|
102
|
+
relativeSourcePaths.push(
|
|
103
|
+
...splitSourcePaths.map((p) => path.relative(options.sourceDirectory, p)),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
splitEsbuildContext = await esbuild.context({
|
|
107
|
+
entryPoints: splitSourcePaths,
|
|
108
|
+
target: 'es2017',
|
|
109
|
+
format: 'esm',
|
|
110
|
+
sourcemap: 'inline',
|
|
111
|
+
bundle: true,
|
|
112
|
+
splitting: true,
|
|
113
|
+
write: false,
|
|
114
|
+
outbase: options.sourceDirectory,
|
|
115
|
+
outdir: options.buildDirectory,
|
|
116
|
+
entryNames: '[dir]/[name]',
|
|
117
|
+
define: {
|
|
118
|
+
'process.env.NODE_ENV': '"development"',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
splitEsbuildServer = await splitEsbuildContext.serve({ host: '0.0.0.0' });
|
|
79
122
|
}
|
|
80
123
|
}
|
|
81
124
|
|
|
@@ -84,6 +127,7 @@ export async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<
|
|
|
84
127
|
*/
|
|
85
128
|
export async function close() {
|
|
86
129
|
esbuildContext?.dispose();
|
|
130
|
+
splitEsbuildContext?.dispose();
|
|
87
131
|
}
|
|
88
132
|
|
|
89
133
|
export function assertConfigured(): void {
|
|
@@ -110,15 +154,21 @@ export function handler() {
|
|
|
110
154
|
});
|
|
111
155
|
}
|
|
112
156
|
|
|
113
|
-
if (!esbuildServer) {
|
|
157
|
+
if (!esbuildServer || !splitEsbuildServer) {
|
|
114
158
|
throw new Error('esbuild server not initialized');
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
const { port } = esbuildServer;
|
|
162
|
+
const { port: splitPort } = splitEsbuildServer;
|
|
118
163
|
|
|
119
164
|
// We're running in dev mode, so we need to boot up esbuild to start building
|
|
120
165
|
// and watching our assets.
|
|
121
166
|
return function (req: IncomingMessage, res: ServerResponse) {
|
|
167
|
+
const isSplitBundle =
|
|
168
|
+
req.url?.startsWith('/scripts/esm-bundles') ||
|
|
169
|
+
// Chunked assets must be served by the split server.
|
|
170
|
+
req.url?.startsWith('/chunk-');
|
|
171
|
+
|
|
122
172
|
// esbuild will reject requests that come from hosts other than the host on
|
|
123
173
|
// which the esbuild dev server is listening:
|
|
124
174
|
// https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d
|
|
@@ -135,7 +185,7 @@ export function handler() {
|
|
|
135
185
|
const proxyReq = http.request(
|
|
136
186
|
{
|
|
137
187
|
hostname: '127.0.0.1',
|
|
138
|
-
port,
|
|
188
|
+
port: isSplitBundle ? splitPort : port,
|
|
139
189
|
path: req.url,
|
|
140
190
|
method: req.method,
|
|
141
191
|
headers,
|
|
@@ -178,12 +228,12 @@ function compiledPath(type: 'scripts' | 'stylesheets', sourceFile: string): stri
|
|
|
178
228
|
}
|
|
179
229
|
|
|
180
230
|
const manifest = readManifest();
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
231
|
+
const asset = manifest[sourceFilePath];
|
|
232
|
+
if (!asset) {
|
|
183
233
|
throw new Error(`Unknown ${type} asset: ${sourceFile}`);
|
|
184
234
|
}
|
|
185
235
|
|
|
186
|
-
return options.publicPath + assetPath;
|
|
236
|
+
return options.publicPath + asset.assetPath;
|
|
187
237
|
}
|
|
188
238
|
|
|
189
239
|
export function compiledScriptPath(sourceFile: string): string {
|
|
@@ -195,6 +245,7 @@ export function compiledStylesheetPath(sourceFile: string): string {
|
|
|
195
245
|
}
|
|
196
246
|
|
|
197
247
|
export function compiledScriptTag(sourceFile: string): HtmlSafeString {
|
|
248
|
+
// Creates a script tag for an IIFE bundle.
|
|
198
249
|
return html`<script src="${compiledScriptPath(sourceFile)}"></script>`;
|
|
199
250
|
}
|
|
200
251
|
|
|
@@ -202,10 +253,32 @@ export function compiledStylesheetTag(sourceFile: string): HtmlSafeString {
|
|
|
202
253
|
return html`<link rel="stylesheet" href="${compiledStylesheetPath(sourceFile)}" />`;
|
|
203
254
|
}
|
|
204
255
|
|
|
205
|
-
|
|
256
|
+
export function compiledScriptModuleTag(sourceFile: string): HtmlSafeString {
|
|
257
|
+
// Creates a module script tag for an ESM bundle.
|
|
258
|
+
return html`<script type="module" src="${compiledScriptPath(sourceFile)}"></script>`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function compiledScriptPreloadPaths(sourceFile: string): string[] {
|
|
262
|
+
assertConfigured();
|
|
263
|
+
|
|
264
|
+
// In dev mode, we don't have a manifest, so we can't preload anything.
|
|
265
|
+
if (options.dev) return [];
|
|
266
|
+
|
|
267
|
+
const manifest = readManifest();
|
|
268
|
+
const asset = manifest[`scripts/${sourceFile}`];
|
|
269
|
+
if (!asset) {
|
|
270
|
+
throw new Error(`Unknown script asset: ${sourceFile}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return asset.preloads.map((preload) => options.publicPath + preload);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function buildAssets(sourceDirectory: string, buildDirectory: string): Promise<Metafile> {
|
|
206
277
|
await fs.ensureDir(buildDirectory);
|
|
207
278
|
|
|
208
|
-
const
|
|
279
|
+
const scriptFiles = await globby(path.join(sourceDirectory, 'scripts', '*.{js,jsx,ts,tsx}'));
|
|
280
|
+
const styleFiles = await globby(path.join(sourceDirectory, 'stylesheets', '*.css'));
|
|
281
|
+
const files = [...scriptFiles, ...styleFiles];
|
|
209
282
|
const buildResult = await esbuild.build({
|
|
210
283
|
entryPoints: files,
|
|
211
284
|
target: 'es2017',
|
|
@@ -220,25 +293,80 @@ async function buildAssets(sourceDirectory: string, buildDirectory: string) {
|
|
|
220
293
|
entryNames: '[dir]/[name]-[hash]',
|
|
221
294
|
outbase: sourceDirectory,
|
|
222
295
|
outdir: buildDirectory,
|
|
296
|
+
define: {
|
|
297
|
+
'process.env.NODE_ENV': '"production"',
|
|
298
|
+
},
|
|
299
|
+
metafile: true, // Write metadata about the build
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)
|
|
303
|
+
// Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,
|
|
304
|
+
// which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.
|
|
305
|
+
const scriptBundleFiles = await globby(
|
|
306
|
+
path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'),
|
|
307
|
+
);
|
|
308
|
+
const esmBundleBuildResult = await esbuild.build({
|
|
309
|
+
entryPoints: scriptBundleFiles,
|
|
310
|
+
target: 'es2017',
|
|
311
|
+
format: 'esm',
|
|
312
|
+
sourcemap: 'linked',
|
|
313
|
+
bundle: true,
|
|
314
|
+
splitting: true,
|
|
315
|
+
minify: true,
|
|
316
|
+
entryNames: '[dir]/[name]-[hash]',
|
|
317
|
+
outbase: sourceDirectory,
|
|
318
|
+
outdir: buildDirectory,
|
|
319
|
+
define: {
|
|
320
|
+
'process.env.NODE_ENV': '"production"',
|
|
321
|
+
},
|
|
223
322
|
metafile: true,
|
|
224
323
|
});
|
|
225
324
|
|
|
226
|
-
|
|
325
|
+
// Merge the resulting metafiles.
|
|
326
|
+
const metafile: Metafile = {
|
|
327
|
+
inputs: { ...buildResult.metafile.inputs, ...esmBundleBuildResult.metafile.inputs },
|
|
328
|
+
outputs: { ...buildResult.metafile.outputs, ...esmBundleBuildResult.metafile.outputs },
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return metafile;
|
|
227
332
|
}
|
|
228
333
|
|
|
229
334
|
function makeManifest(
|
|
230
335
|
metafile: Metafile,
|
|
231
336
|
sourceDirectory: string,
|
|
232
337
|
buildDirectory: string,
|
|
233
|
-
):
|
|
234
|
-
const manifest:
|
|
338
|
+
): AssetsManifest {
|
|
339
|
+
const manifest: AssetsManifest = {};
|
|
340
|
+
|
|
235
341
|
Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
|
|
236
342
|
if (!meta.entryPoint) return;
|
|
237
343
|
|
|
344
|
+
// Compute all the necessary preloads for each entrypoint. This includes
|
|
345
|
+
// any code-split chunks, as well as any files that are dynamically imported.
|
|
346
|
+
const preloads = new Set<string>();
|
|
347
|
+
|
|
348
|
+
// Recursively walk the `imports` dependency tree
|
|
349
|
+
const visit = (entry: (typeof meta)['imports'][number]) => {
|
|
350
|
+
if (!['import-statement', 'dynamic-import'].includes(entry.kind)) return;
|
|
351
|
+
if (preloads.has(entry.path)) return;
|
|
352
|
+
preloads.add(entry.path);
|
|
353
|
+
for (const imp of metafile.inputs[entry.path]?.imports ?? []) {
|
|
354
|
+
visit(imp);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
for (const imp of meta.imports) {
|
|
359
|
+
visit(imp);
|
|
360
|
+
}
|
|
361
|
+
|
|
238
362
|
const entryPath = path.relative(sourceDirectory, meta.entryPoint);
|
|
239
363
|
const assetPath = path.relative(buildDirectory, outputPath);
|
|
240
|
-
manifest[entryPath] =
|
|
364
|
+
manifest[entryPath] = {
|
|
365
|
+
assetPath,
|
|
366
|
+
preloads: [...preloads],
|
|
367
|
+
};
|
|
241
368
|
});
|
|
369
|
+
|
|
242
370
|
return manifest;
|
|
243
371
|
}
|
|
244
372
|
|
package/.mocharc.cjs
DELETED