@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 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
- ├── assets/
24
- │ ├── scripts/
25
- | │ ├── lib/
26
- | │ │ ├── shared-code.ts
27
- | │ │ └── more-shared-code.ts
28
- | │ ├── foo.ts
29
- │ | └── bar.ts
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 { compiledScriptTag, compiledScriptPath } from '@prairielearn/compiled-assets';
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 (filePath) => {
14
- const destinationFilePath = path.resolve(destination, filePath);
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[filePath] = {
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, assetPath]) => {
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,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClC,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAI9C,KAAK,UAAU,qBAAqB,CAClC,WAAmB,EACnB,QAAgC;IAEhC,MAAM,eAAe,GAAoB,EAAE,CAAC;IAC5C,MAAM,OAAO,CAAC,GAAG,CACf,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QAC7C,MAAM,mBAAmB,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QAChE,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;QACxD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,gBAAgB,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,mBAAmB,KAAK,EAAE,cAAc,CAAC,CAAC;QAChE,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,mBAAmB,KAAK,EAAE,gBAAgB,CAAC,CAAC;QAClE,eAAe,CAAC,QAAQ,CAAC,GAAG;YAC1B,GAAG,EAAE,QAAQ,CAAC,MAAM;YACpB,IAAI,EAAE,cAAc,CAAC,MAAM;YAC3B,MAAM,EAAE,gBAAgB,CAAC,MAAM;SAChC,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IACF,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,OAAO,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE;IACnF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAElD,8EAA8E;IAC9E,iCAAiC;IACjC,MAAM,eAAe,GAAG,MAAM,qBAAqB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAE3E,wEAAwE;IACxE,MAAM,OAAO,GAAwB,EAAE,CAAC;IACxC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE;QAC3D,MAAM,KAAK,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;QACzC,OAAO,CAAC,UAAU,CAAC,GAAG;YACpB,aAAa,EAAE,SAAS;YACxB,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC;YAC5B,aAAa,EAAE,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC;YACtC,eAAe,EAAE,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC;SAC3C,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAC7C,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","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 { build } from './index.js';\n\nconst gzip = promisify(zlib.gzip);\nconst brotli = promisify(zlib.brotliCompress);\n\ntype CompressedSizes = Record<string, Record<string, number>>;\n\nasync function writeCompressedAssets(\n destination: string,\n manifest: Record<string, string>,\n): Promise<CompressedSizes> {\n const compressedSizes: CompressedSizes = {};\n await Promise.all(\n Object.values(manifest).map(async (filePath) => {\n const destinationFilePath = path.resolve(destination, filePath);\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[filePath] = {\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, assetPath]) => {\n const sizes = compressedSizes[assetPath];\n results[entryPoint] = {\n 'Output file': 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"]}
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, 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 assetPath = manifest[sourceFilePath];
137
- if (!assetPath) {
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 files = await globby(path.join(sourceDirectory, '*/*.{js,jsx,ts,tsx,css}'));
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
- return buildResult.metafile;
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] = assetPath;
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":"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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 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.18",
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": "^20.17.57",
30
- "@vitest/coverage-v8": "^3.2.1",
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.1"
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: Record<string, string>,
28
+ manifest: AssetsManifest,
20
29
  ): Promise<CompressedSizes> {
21
30
  const compressedSizes: CompressedSizes = {};
22
31
  await Promise.all(
23
- Object.values(manifest).map(async (filePath) => {
24
- const destinationFilePath = path.resolve(destination, filePath);
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[filePath] = {
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, assetPath]) => {
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<string, string>;
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 assetPath = manifest[sourceFilePath];
182
- if (!assetPath) {
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
- async function buildAssets(sourceDirectory: string, buildDirectory: string) {
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 files = await globby(path.join(sourceDirectory, '*/*.{js,jsx,ts,tsx,css}'));
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
- return buildResult.metafile;
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
- ): Record<string, string> {
234
- const manifest: Record<string, string> = {};
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] = assetPath;
364
+ manifest[entryPath] = {
365
+ assetPath,
366
+ preloads: [...preloads],
367
+ };
241
368
  });
369
+
242
370
  return manifest;
243
371
  }
244
372
 
package/.mocharc.cjs DELETED
@@ -1,3 +0,0 @@
1
- module.exports = {
2
- require: ['tsx'],
3
- };