@prairielearn/compiled-assets 3.0.17 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @prairielearn/compiled-assets
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 791b1c7: Support preloading assets, and assets processed with ESM + code splitting
8
+
9
+ ## 3.0.18
10
+
11
+ ### Patch Changes
12
+
13
+ - d97b97a: Upgrade all JavaScript dependencies
14
+
3
15
  ## 3.0.17
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 = {
@@ -49,6 +51,22 @@ export async function init(newOptions) {
49
51
  entryNames: '[dir]/[name]',
50
52
  });
51
53
  esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });
54
+ const splitSourceGlob = path.join(options.sourceDirectory, 'scripts', 'esm-bundles', '**', '*.{js,ts,jsx,tsx}');
55
+ const splitSourcePaths = await globby(splitSourceGlob);
56
+ relativeSourcePaths.push(...splitSourcePaths.map((p) => path.relative(options.sourceDirectory, p)));
57
+ splitEsbuildContext = await esbuild.context({
58
+ entryPoints: splitSourcePaths,
59
+ target: 'es2017',
60
+ format: 'esm',
61
+ sourcemap: 'inline',
62
+ bundle: true,
63
+ splitting: true,
64
+ write: false,
65
+ outbase: options.sourceDirectory,
66
+ outdir: options.buildDirectory,
67
+ entryNames: '[dir]/[name]',
68
+ });
69
+ splitEsbuildServer = await splitEsbuildContext.serve({ host: '0.0.0.0' });
52
70
  }
53
71
  }
54
72
  /**
@@ -56,6 +74,7 @@ export async function init(newOptions) {
56
74
  */
57
75
  export async function close() {
58
76
  esbuildContext?.dispose();
77
+ splitEsbuildContext?.dispose();
59
78
  }
60
79
  export function assertConfigured() {
61
80
  if (!options) {
@@ -78,13 +97,17 @@ export function handler() {
78
97
  },
79
98
  });
80
99
  }
81
- if (!esbuildServer) {
100
+ if (!esbuildServer || !splitEsbuildServer) {
82
101
  throw new Error('esbuild server not initialized');
83
102
  }
84
103
  const { port } = esbuildServer;
104
+ const { port: splitPort } = splitEsbuildServer;
85
105
  // We're running in dev mode, so we need to boot up esbuild to start building
86
106
  // and watching our assets.
87
107
  return function (req, res) {
108
+ const isSplitBundle = req.url?.startsWith('/scripts/esm-bundles') ||
109
+ // Chunked assets must be served by the split server.
110
+ req.url?.startsWith('/chunk-');
88
111
  // esbuild will reject requests that come from hosts other than the host on
89
112
  // which the esbuild dev server is listening:
90
113
  // https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d
@@ -99,7 +122,7 @@ export function handler() {
99
122
  delete headers['referer'];
100
123
  const proxyReq = http.request({
101
124
  hostname: '127.0.0.1',
102
- port,
125
+ port: isSplitBundle ? splitPort : port,
103
126
  path: req.url,
104
127
  method: req.method,
105
128
  headers,
@@ -133,11 +156,11 @@ function compiledPath(type, sourceFile) {
133
156
  return options.publicPath + sourceFilePath.replace(/\.(js|ts)x?$/, '.js');
134
157
  }
135
158
  const manifest = readManifest();
136
- const assetPath = manifest[sourceFilePath];
137
- if (!assetPath) {
159
+ const asset = manifest[sourceFilePath];
160
+ if (!asset) {
138
161
  throw new Error(`Unknown ${type} asset: ${sourceFile}`);
139
162
  }
140
- return options.publicPath + assetPath;
163
+ return options.publicPath + asset.assetPath;
141
164
  }
142
165
  export function compiledScriptPath(sourceFile) {
143
166
  return compiledPath('scripts', sourceFile);
@@ -146,14 +169,33 @@ export function compiledStylesheetPath(sourceFile) {
146
169
  return compiledPath('stylesheets', sourceFile);
147
170
  }
148
171
  export function compiledScriptTag(sourceFile) {
172
+ // Creates a script tag for an IIFE bundle.
149
173
  return html `<script src="${compiledScriptPath(sourceFile)}"></script>`;
150
174
  }
151
175
  export function compiledStylesheetTag(sourceFile) {
152
176
  return html `<link rel="stylesheet" href="${compiledStylesheetPath(sourceFile)}" />`;
153
177
  }
178
+ export function compiledScriptModuleTag(sourceFile) {
179
+ // Creates a module script tag for an ESM bundle.
180
+ return html `<script type="module" src="${compiledScriptPath(sourceFile)}"></script>`;
181
+ }
182
+ export function compiledScriptPreloadPaths(sourceFile) {
183
+ assertConfigured();
184
+ // In dev mode, we don't have a manifest, so we can't preload anything.
185
+ if (options.dev)
186
+ return [];
187
+ const manifest = readManifest();
188
+ const asset = manifest[`scripts/${sourceFile}`];
189
+ if (!asset) {
190
+ throw new Error(`Unknown script asset: ${sourceFile}`);
191
+ }
192
+ return asset.preloads.map((preload) => options.publicPath + preload);
193
+ }
154
194
  async function buildAssets(sourceDirectory, buildDirectory) {
155
195
  await fs.ensureDir(buildDirectory);
156
- const files = await globby(path.join(sourceDirectory, '*/*.{js,jsx,ts,tsx,css}'));
196
+ const scriptFiles = await globby(path.join(sourceDirectory, 'scripts', '*.{js,jsx,ts,tsx}'));
197
+ const styleFiles = await globby(path.join(sourceDirectory, 'stylesheets', '*.css'));
198
+ const files = [...scriptFiles, ...styleFiles];
157
199
  const buildResult = await esbuild.build({
158
200
  entryPoints: files,
159
201
  target: 'es2017',
@@ -168,18 +210,60 @@ async function buildAssets(sourceDirectory, buildDirectory) {
168
210
  entryNames: '[dir]/[name]-[hash]',
169
211
  outbase: sourceDirectory,
170
212
  outdir: buildDirectory,
213
+ metafile: true, // Write metadata about the build
214
+ });
215
+ // For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)
216
+ // Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,
217
+ // which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.
218
+ const scriptBundleFiles = await globby(path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'));
219
+ const esmBundleBuildResult = await esbuild.build({
220
+ entryPoints: scriptBundleFiles,
221
+ target: 'es2017',
222
+ format: 'esm',
223
+ sourcemap: 'linked',
224
+ bundle: true,
225
+ splitting: true,
226
+ minify: true,
227
+ entryNames: '[dir]/[name]-[hash]',
228
+ outbase: sourceDirectory,
229
+ outdir: buildDirectory,
171
230
  metafile: true,
172
231
  });
173
- return buildResult.metafile;
232
+ // Merge the resulting metafiles.
233
+ const metafile = {
234
+ inputs: { ...buildResult.metafile.inputs, ...esmBundleBuildResult.metafile.inputs },
235
+ outputs: { ...buildResult.metafile.outputs, ...esmBundleBuildResult.metafile.outputs },
236
+ };
237
+ return metafile;
174
238
  }
175
239
  function makeManifest(metafile, sourceDirectory, buildDirectory) {
176
240
  const manifest = {};
177
241
  Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
178
242
  if (!meta.entryPoint)
179
243
  return;
244
+ // Compute all the necessary preloads for each entrypoint. This includes
245
+ // any code-split chunks, as well as any files that are dynamically imported.
246
+ const preloads = new Set();
247
+ // Recursively walk the `imports` dependency tree
248
+ const visit = (entry) => {
249
+ if (!['import-statement', 'dynamic-import'].includes(entry.kind))
250
+ return;
251
+ if (preloads.has(entry.path))
252
+ return;
253
+ preloads.add(entry.path);
254
+ for (const imp of metafile.inputs[entry.path]?.imports ?? []) {
255
+ visit(imp);
256
+ }
257
+ };
258
+ for (const imp of meta.imports) {
259
+ visit(imp);
260
+ }
180
261
  const entryPath = path.relative(sourceDirectory, meta.entryPoint);
181
262
  const assetPath = path.relative(buildDirectory, outputPath);
182
- manifest[entryPath] = assetPath;
263
+ manifest[entryPath] = {
264
+ assetPath,
265
+ preloads: [...preloads],
266
+ };
183
267
  });
184
268
  return manifest;
185
269
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import http, { type IncomingMessage, type ServerResponse } from 'node:http';\nimport path from 'path';\n\nimport esbuild, { type Metafile } from 'esbuild';\nimport expressStaticGzip from 'express-static-gzip';\nimport fs from 'fs-extra';\nimport { globby } from 'globby';\n\nimport { type HtmlSafeString, html } from '@prairielearn/html';\n\nconst DEFAULT_OPTIONS = {\n dev: process.env.NODE_ENV !== 'production',\n sourceDirectory: './assets',\n buildDirectory: './public/build',\n publicPath: '/build/',\n};\n\ntype AssetsManifest = Record<string, string>;\n\nexport interface CompiledAssetsOptions {\n /**\n * Whether the app is running in dev mode. If dev mode is enabled, then\n * assets will be built on the fly as they're requested. Otherwise, assets\n * should have been pre-compiled to the `buildDirectory` directory.\n */\n dev?: boolean;\n /** Root directory of assets. */\n sourceDirectory?: string;\n /** Directory where the built assets will be output to. */\n buildDirectory?: string;\n /** The path that assets will be served from, e.g. `/build/`. */\n publicPath?: string;\n}\n\nlet options: Required<CompiledAssetsOptions> = { ...DEFAULT_OPTIONS };\nlet esbuildContext: esbuild.BuildContext | null = null;\nlet esbuildServer: esbuild.ServeResult | null = null;\nlet relativeSourcePaths: string[] | null = null;\n\nexport async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<void> {\n options = {\n ...DEFAULT_OPTIONS,\n ...newOptions,\n };\n\n if (!options.publicPath.endsWith('/')) {\n options.publicPath += '/';\n }\n\n if (options.dev) {\n // Use esbuild's asset server in development.\n //\n // Note that esbuild doesn't support globs, so the server will not pick up\n // new entrypoints that are added while the server is running.\n const sourceGlob = path.join(options.sourceDirectory, '*', '*.{js,ts,jsx,tsx,css}');\n const sourcePaths = await globby(sourceGlob);\n\n // Save the result of globbing for the source paths so that we can later\n // check if a given filename exists.\n relativeSourcePaths = sourcePaths.map((p) => path.relative(options.sourceDirectory, p));\n\n esbuildContext = await esbuild.context({\n entryPoints: sourcePaths,\n target: 'es2017',\n format: 'iife',\n sourcemap: 'inline',\n bundle: true,\n write: false,\n loader: {\n '.woff': 'file',\n '.woff2': 'file',\n },\n outbase: options.sourceDirectory,\n outdir: options.buildDirectory,\n entryNames: '[dir]/[name]',\n });\n\n esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });\n }\n}\n\n/**\n * Shuts down the development assets compiler if it is running.\n */\nexport async function close() {\n esbuildContext?.dispose();\n}\n\nexport function assertConfigured(): void {\n if (!options) {\n throw new Error('@prairielearn/compiled-assets was not configured');\n }\n}\n\nexport function handler() {\n assertConfigured();\n\n if (!options?.dev) {\n // We're running in production: serve all assets from the build directory.\n // Set headers to cache for as long as possible, since the assets will\n // include content hashes in their filenames.\n return expressStaticGzip(options?.buildDirectory, {\n enableBrotli: true,\n // Prefer Brotli if the client supports it.\n orderPreference: ['br'],\n serveStatic: {\n maxAge: '31557600',\n immutable: true,\n },\n });\n }\n\n if (!esbuildServer) {\n throw new Error('esbuild server not initialized');\n }\n\n const { port } = esbuildServer;\n\n // We're running in dev mode, so we need to boot up esbuild to start building\n // and watching our assets.\n return function (req: IncomingMessage, res: ServerResponse) {\n // esbuild will reject requests that come from hosts other than the host on\n // which the esbuild dev server is listening:\n // https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d\n // https://github.com/evanw/esbuild/releases/tag/v0.25.0\n // We work around this by modifying the request headers to make it look like\n // the request is coming from localhost, which esbuild won't reject.\n const headers = structuredClone(req.headers);\n headers.host = 'localhost';\n delete headers['x-forwarded-for'];\n delete headers['x-forwarded-host'];\n delete headers['x-forwarded-proto'];\n delete headers['referer'];\n\n const proxyReq = http.request(\n {\n hostname: '127.0.0.1',\n port,\n path: req.url,\n method: req.method,\n headers,\n },\n (proxyRes) => {\n res.writeHead(proxyRes.statusCode ?? 500, proxyRes.headers);\n proxyRes.pipe(res, { end: true });\n },\n );\n req.pipe(proxyReq, { end: true });\n };\n}\n\nlet cachedManifest: AssetsManifest | null = null;\nfunction readManifest(): AssetsManifest {\n assertConfigured();\n\n if (!cachedManifest) {\n const manifestPath = path.join(options.buildDirectory, 'manifest.json');\n cachedManifest = fs.readJSONSync(manifestPath) as AssetsManifest;\n }\n\n return cachedManifest;\n}\n\nfunction compiledPath(type: 'scripts' | 'stylesheets', sourceFile: string): string {\n assertConfigured();\n const sourceFilePath = `${type}/${sourceFile}`;\n\n if (options.dev) {\n // To ensure that errors that would be raised in production are also raised\n // in development, we'll check for the existence of the asset file on disk.\n // This mirrors the production check of the file in the manifest: if a file\n // exists on disk, it should be in the manifest.\n if (!relativeSourcePaths?.find((p) => p === sourceFilePath)) {\n throw new Error(`Unknown ${type} asset: ${sourceFile}`);\n }\n\n return options.publicPath + sourceFilePath.replace(/\\.(js|ts)x?$/, '.js');\n }\n\n const manifest = readManifest();\n const assetPath = manifest[sourceFilePath];\n if (!assetPath) {\n throw new Error(`Unknown ${type} asset: ${sourceFile}`);\n }\n\n return options.publicPath + assetPath;\n}\n\nexport function compiledScriptPath(sourceFile: string): string {\n return compiledPath('scripts', sourceFile);\n}\n\nexport function compiledStylesheetPath(sourceFile: string): string {\n return compiledPath('stylesheets', sourceFile);\n}\n\nexport function compiledScriptTag(sourceFile: string): HtmlSafeString {\n return html`<script src=\"${compiledScriptPath(sourceFile)}\"></script>`;\n}\n\nexport function compiledStylesheetTag(sourceFile: string): HtmlSafeString {\n return html`<link rel=\"stylesheet\" href=\"${compiledStylesheetPath(sourceFile)}\" />`;\n}\n\nasync function buildAssets(sourceDirectory: string, buildDirectory: string) {\n await fs.ensureDir(buildDirectory);\n\n const files = await globby(path.join(sourceDirectory, '*/*.{js,jsx,ts,tsx,css}'));\n const buildResult = await esbuild.build({\n entryPoints: files,\n target: 'es2017',\n format: 'iife',\n sourcemap: 'linked',\n bundle: true,\n minify: true,\n loader: {\n '.woff': 'file',\n '.woff2': 'file',\n },\n entryNames: '[dir]/[name]-[hash]',\n outbase: sourceDirectory,\n outdir: buildDirectory,\n metafile: true,\n });\n\n return buildResult.metafile;\n}\n\nfunction makeManifest(\n metafile: Metafile,\n sourceDirectory: string,\n buildDirectory: string,\n): Record<string, string> {\n const manifest: Record<string, string> = {};\n Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {\n if (!meta.entryPoint) return;\n\n const entryPath = path.relative(sourceDirectory, meta.entryPoint);\n const assetPath = path.relative(buildDirectory, outputPath);\n manifest[entryPath] = assetPath;\n });\n return manifest;\n}\n\nexport async function build(\n sourceDirectory: string,\n buildDirectory: string,\n): Promise<AssetsManifest> {\n // Remove existing assets to ensure that no stale assets are left behind.\n await fs.remove(buildDirectory);\n\n const metafile = await buildAssets(sourceDirectory, buildDirectory);\n const manifest = makeManifest(metafile, sourceDirectory, buildDirectory);\n const manifestPath = path.join(buildDirectory, 'manifest.json');\n await fs.writeJSON(manifestPath, manifest);\n\n return manifest;\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import http, { type IncomingMessage, type ServerResponse } from 'node:http';\nimport path from 'path';\n\nimport esbuild, { type Metafile } from 'esbuild';\nimport expressStaticGzip from 'express-static-gzip';\nimport fs from 'fs-extra';\nimport { globby } from 'globby';\n\nimport { type HtmlSafeString, html } from '@prairielearn/html';\n\nconst DEFAULT_OPTIONS = {\n dev: process.env.NODE_ENV !== 'production',\n sourceDirectory: './assets',\n buildDirectory: './public/build',\n publicPath: '/build/',\n};\n\nexport type AssetsManifest = Record<\n string,\n {\n assetPath: string;\n preloads: string[];\n }\n>;\n\nexport interface CompiledAssetsOptions {\n /**\n * Whether the app is running in dev mode. If dev mode is enabled, then\n * assets will be built on the fly as they're requested. Otherwise, assets\n * should have been pre-compiled to the `buildDirectory` directory.\n */\n dev?: boolean;\n /** Root directory of assets. */\n sourceDirectory?: string;\n /** Directory where the built assets will be output to. */\n buildDirectory?: string;\n /** The path that assets will be served from, e.g. `/build/`. */\n publicPath?: string;\n}\n\nlet options: Required<CompiledAssetsOptions> = { ...DEFAULT_OPTIONS };\n\nlet esbuildContext: esbuild.BuildContext | null = null;\nlet esbuildServer: esbuild.ServeResult | null = null;\n\nlet splitEsbuildContext: esbuild.BuildContext | null = null;\nlet splitEsbuildServer: esbuild.ServeResult | null = null;\n\nlet relativeSourcePaths: string[] | null = null;\n\nexport async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<void> {\n options = {\n ...DEFAULT_OPTIONS,\n ...newOptions,\n };\n\n if (!options.publicPath.endsWith('/')) {\n options.publicPath += '/';\n }\n\n if (options.dev) {\n // Use esbuild's asset server in development.\n //\n // Note that esbuild doesn't support globs, so the server will not pick up\n // new entrypoints that are added while the server is running.\n const sourceGlob = path.join(options.sourceDirectory, '*', '*.{js,ts,jsx,tsx,css}');\n const sourcePaths = await globby(sourceGlob);\n\n // Save the result of globbing for the source paths so that we can later\n // check if a given filename exists.\n relativeSourcePaths = sourcePaths.map((p) => path.relative(options.sourceDirectory, p));\n\n esbuildContext = await esbuild.context({\n entryPoints: sourcePaths,\n target: 'es2017',\n format: 'iife',\n sourcemap: 'inline',\n bundle: true,\n write: false,\n loader: {\n '.woff': 'file',\n '.woff2': 'file',\n },\n outbase: options.sourceDirectory,\n outdir: options.buildDirectory,\n entryNames: '[dir]/[name]',\n });\n esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });\n\n const splitSourceGlob = path.join(\n options.sourceDirectory,\n 'scripts',\n 'esm-bundles',\n '**',\n '*.{js,ts,jsx,tsx}',\n );\n const splitSourcePaths = await globby(splitSourceGlob);\n\n relativeSourcePaths.push(\n ...splitSourcePaths.map((p) => path.relative(options.sourceDirectory, p)),\n );\n\n splitEsbuildContext = await esbuild.context({\n entryPoints: splitSourcePaths,\n target: 'es2017',\n format: 'esm',\n sourcemap: 'inline',\n bundle: true,\n splitting: true,\n write: false,\n outbase: options.sourceDirectory,\n outdir: options.buildDirectory,\n entryNames: '[dir]/[name]',\n });\n splitEsbuildServer = await splitEsbuildContext.serve({ host: '0.0.0.0' });\n }\n}\n\n/**\n * Shuts down the development assets compiler if it is running.\n */\nexport async function close() {\n esbuildContext?.dispose();\n splitEsbuildContext?.dispose();\n}\n\nexport function assertConfigured(): void {\n if (!options) {\n throw new Error('@prairielearn/compiled-assets was not configured');\n }\n}\n\nexport function handler() {\n assertConfigured();\n\n if (!options?.dev) {\n // We're running in production: serve all assets from the build directory.\n // Set headers to cache for as long as possible, since the assets will\n // include content hashes in their filenames.\n return expressStaticGzip(options?.buildDirectory, {\n enableBrotli: true,\n // Prefer Brotli if the client supports it.\n orderPreference: ['br'],\n serveStatic: {\n maxAge: '31557600',\n immutable: true,\n },\n });\n }\n\n if (!esbuildServer || !splitEsbuildServer) {\n throw new Error('esbuild server not initialized');\n }\n\n const { port } = esbuildServer;\n const { port: splitPort } = splitEsbuildServer;\n\n // We're running in dev mode, so we need to boot up esbuild to start building\n // and watching our assets.\n return function (req: IncomingMessage, res: ServerResponse) {\n const isSplitBundle =\n req.url?.startsWith('/scripts/esm-bundles') ||\n // Chunked assets must be served by the split server.\n req.url?.startsWith('/chunk-');\n\n // esbuild will reject requests that come from hosts other than the host on\n // which the esbuild dev server is listening:\n // https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d\n // https://github.com/evanw/esbuild/releases/tag/v0.25.0\n // We work around this by modifying the request headers to make it look like\n // the request is coming from localhost, which esbuild won't reject.\n const headers = structuredClone(req.headers);\n headers.host = 'localhost';\n delete headers['x-forwarded-for'];\n delete headers['x-forwarded-host'];\n delete headers['x-forwarded-proto'];\n delete headers['referer'];\n\n const proxyReq = http.request(\n {\n hostname: '127.0.0.1',\n port: isSplitBundle ? splitPort : port,\n path: req.url,\n method: req.method,\n headers,\n },\n (proxyRes) => {\n res.writeHead(proxyRes.statusCode ?? 500, proxyRes.headers);\n proxyRes.pipe(res, { end: true });\n },\n );\n req.pipe(proxyReq, { end: true });\n };\n}\n\nlet cachedManifest: AssetsManifest | null = null;\nfunction readManifest(): AssetsManifest {\n assertConfigured();\n\n if (!cachedManifest) {\n const manifestPath = path.join(options.buildDirectory, 'manifest.json');\n cachedManifest = fs.readJSONSync(manifestPath) as AssetsManifest;\n }\n\n return cachedManifest;\n}\n\nfunction compiledPath(type: 'scripts' | 'stylesheets', sourceFile: string): string {\n assertConfigured();\n const sourceFilePath = `${type}/${sourceFile}`;\n\n if (options.dev) {\n // To ensure that errors that would be raised in production are also raised\n // in development, we'll check for the existence of the asset file on disk.\n // This mirrors the production check of the file in the manifest: if a file\n // exists on disk, it should be in the manifest.\n if (!relativeSourcePaths?.find((p) => p === sourceFilePath)) {\n throw new Error(`Unknown ${type} asset: ${sourceFile}`);\n }\n\n return options.publicPath + sourceFilePath.replace(/\\.(js|ts)x?$/, '.js');\n }\n\n const manifest = readManifest();\n const asset = manifest[sourceFilePath];\n if (!asset) {\n throw new Error(`Unknown ${type} asset: ${sourceFile}`);\n }\n\n return options.publicPath + asset.assetPath;\n}\n\nexport function compiledScriptPath(sourceFile: string): string {\n return compiledPath('scripts', sourceFile);\n}\n\nexport function compiledStylesheetPath(sourceFile: string): string {\n return compiledPath('stylesheets', sourceFile);\n}\n\nexport function compiledScriptTag(sourceFile: string): HtmlSafeString {\n // Creates a script tag for an IIFE bundle.\n return html`<script src=\"${compiledScriptPath(sourceFile)}\"></script>`;\n}\n\nexport function compiledStylesheetTag(sourceFile: string): HtmlSafeString {\n return html`<link rel=\"stylesheet\" href=\"${compiledStylesheetPath(sourceFile)}\" />`;\n}\n\nexport function compiledScriptModuleTag(sourceFile: string): HtmlSafeString {\n // Creates a module script tag for an ESM bundle.\n return html`<script type=\"module\" src=\"${compiledScriptPath(sourceFile)}\"></script>`;\n}\n\nexport function compiledScriptPreloadPaths(sourceFile: string): string[] {\n assertConfigured();\n\n // In dev mode, we don't have a manifest, so we can't preload anything.\n if (options.dev) return [];\n\n const manifest = readManifest();\n const asset = manifest[`scripts/${sourceFile}`];\n if (!asset) {\n throw new Error(`Unknown script asset: ${sourceFile}`);\n }\n\n return asset.preloads.map((preload) => options.publicPath + preload);\n}\n\nasync function buildAssets(sourceDirectory: string, buildDirectory: string): Promise<Metafile> {\n await fs.ensureDir(buildDirectory);\n\n const scriptFiles = await globby(path.join(sourceDirectory, 'scripts', '*.{js,jsx,ts,tsx}'));\n const styleFiles = await globby(path.join(sourceDirectory, 'stylesheets', '*.css'));\n const files = [...scriptFiles, ...styleFiles];\n const buildResult = await esbuild.build({\n entryPoints: files,\n target: 'es2017',\n format: 'iife',\n sourcemap: 'linked',\n bundle: true,\n minify: true,\n loader: {\n '.woff': 'file',\n '.woff2': 'file',\n },\n entryNames: '[dir]/[name]-[hash]',\n outbase: sourceDirectory,\n outdir: buildDirectory,\n metafile: true, // Write metadata about the build\n });\n\n // For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)\n // Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,\n // which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.\n const scriptBundleFiles = await globby(\n path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'),\n );\n const esmBundleBuildResult = await esbuild.build({\n entryPoints: scriptBundleFiles,\n target: 'es2017',\n format: 'esm',\n sourcemap: 'linked',\n bundle: true,\n splitting: true,\n minify: true,\n entryNames: '[dir]/[name]-[hash]',\n outbase: sourceDirectory,\n outdir: buildDirectory,\n metafile: true,\n });\n\n // Merge the resulting metafiles.\n const metafile: Metafile = {\n inputs: { ...buildResult.metafile.inputs, ...esmBundleBuildResult.metafile.inputs },\n outputs: { ...buildResult.metafile.outputs, ...esmBundleBuildResult.metafile.outputs },\n };\n\n return metafile;\n}\n\nfunction makeManifest(\n metafile: Metafile,\n sourceDirectory: string,\n buildDirectory: string,\n): AssetsManifest {\n const manifest: AssetsManifest = {};\n\n Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {\n if (!meta.entryPoint) return;\n\n // Compute all the necessary preloads for each entrypoint. This includes\n // any code-split chunks, as well as any files that are dynamically imported.\n const preloads = new Set<string>();\n\n // Recursively walk the `imports` dependency tree\n const visit = (entry: (typeof meta)['imports'][number]) => {\n if (!['import-statement', 'dynamic-import'].includes(entry.kind)) return;\n if (preloads.has(entry.path)) return;\n preloads.add(entry.path);\n for (const imp of metafile.inputs[entry.path]?.imports ?? []) {\n visit(imp);\n }\n };\n\n for (const imp of meta.imports) {\n visit(imp);\n }\n\n const entryPath = path.relative(sourceDirectory, meta.entryPoint);\n const assetPath = path.relative(buildDirectory, outputPath);\n manifest[entryPath] = {\n assetPath,\n preloads: [...preloads],\n };\n });\n\n return manifest;\n}\n\nexport async function build(\n sourceDirectory: string,\n buildDirectory: string,\n): Promise<AssetsManifest> {\n // Remove existing assets to ensure that no stale assets are left behind.\n await fs.remove(buildDirectory);\n\n const metafile = await buildAssets(sourceDirectory, buildDirectory);\n const manifest = makeManifest(metafile, sourceDirectory, buildDirectory);\n const manifestPath = path.join(buildDirectory, 'manifest.json');\n await fs.writeJSON(manifestPath, manifest);\n\n return manifest;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/compiled-assets",
3
- "version": "3.0.17",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "@prairielearn/html": "^4.0.15",
19
19
  "commander": "^13.1.0",
20
- "esbuild": "^0.25.3",
20
+ "esbuild": "^0.25.5",
21
21
  "express-static-gzip": "^2.2.0",
22
22
  "fs-extra": "^11.3.0",
23
23
  "globby": "^14.1.0",
@@ -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.1.4",
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.1.4"
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> {
@@ -74,8 +85,34 @@ export async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<
74
85
  outdir: options.buildDirectory,
75
86
  entryNames: '[dir]/[name]',
76
87
  });
77
-
78
88
  esbuildServer = await esbuildContext.serve({ host: '0.0.0.0' });
89
+
90
+ const splitSourceGlob = path.join(
91
+ options.sourceDirectory,
92
+ 'scripts',
93
+ 'esm-bundles',
94
+ '**',
95
+ '*.{js,ts,jsx,tsx}',
96
+ );
97
+ const splitSourcePaths = await globby(splitSourceGlob);
98
+
99
+ relativeSourcePaths.push(
100
+ ...splitSourcePaths.map((p) => path.relative(options.sourceDirectory, p)),
101
+ );
102
+
103
+ splitEsbuildContext = await esbuild.context({
104
+ entryPoints: splitSourcePaths,
105
+ target: 'es2017',
106
+ format: 'esm',
107
+ sourcemap: 'inline',
108
+ bundle: true,
109
+ splitting: true,
110
+ write: false,
111
+ outbase: options.sourceDirectory,
112
+ outdir: options.buildDirectory,
113
+ entryNames: '[dir]/[name]',
114
+ });
115
+ splitEsbuildServer = await splitEsbuildContext.serve({ host: '0.0.0.0' });
79
116
  }
80
117
  }
81
118
 
@@ -84,6 +121,7 @@ export async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<
84
121
  */
85
122
  export async function close() {
86
123
  esbuildContext?.dispose();
124
+ splitEsbuildContext?.dispose();
87
125
  }
88
126
 
89
127
  export function assertConfigured(): void {
@@ -110,15 +148,21 @@ export function handler() {
110
148
  });
111
149
  }
112
150
 
113
- if (!esbuildServer) {
151
+ if (!esbuildServer || !splitEsbuildServer) {
114
152
  throw new Error('esbuild server not initialized');
115
153
  }
116
154
 
117
155
  const { port } = esbuildServer;
156
+ const { port: splitPort } = splitEsbuildServer;
118
157
 
119
158
  // We're running in dev mode, so we need to boot up esbuild to start building
120
159
  // and watching our assets.
121
160
  return function (req: IncomingMessage, res: ServerResponse) {
161
+ const isSplitBundle =
162
+ req.url?.startsWith('/scripts/esm-bundles') ||
163
+ // Chunked assets must be served by the split server.
164
+ req.url?.startsWith('/chunk-');
165
+
122
166
  // esbuild will reject requests that come from hosts other than the host on
123
167
  // which the esbuild dev server is listening:
124
168
  // https://github.com/evanw/esbuild/commit/de85afd65edec9ebc44a11e245fd9e9a2e99760d
@@ -135,7 +179,7 @@ export function handler() {
135
179
  const proxyReq = http.request(
136
180
  {
137
181
  hostname: '127.0.0.1',
138
- port,
182
+ port: isSplitBundle ? splitPort : port,
139
183
  path: req.url,
140
184
  method: req.method,
141
185
  headers,
@@ -178,12 +222,12 @@ function compiledPath(type: 'scripts' | 'stylesheets', sourceFile: string): stri
178
222
  }
179
223
 
180
224
  const manifest = readManifest();
181
- const assetPath = manifest[sourceFilePath];
182
- if (!assetPath) {
225
+ const asset = manifest[sourceFilePath];
226
+ if (!asset) {
183
227
  throw new Error(`Unknown ${type} asset: ${sourceFile}`);
184
228
  }
185
229
 
186
- return options.publicPath + assetPath;
230
+ return options.publicPath + asset.assetPath;
187
231
  }
188
232
 
189
233
  export function compiledScriptPath(sourceFile: string): string {
@@ -195,6 +239,7 @@ export function compiledStylesheetPath(sourceFile: string): string {
195
239
  }
196
240
 
197
241
  export function compiledScriptTag(sourceFile: string): HtmlSafeString {
242
+ // Creates a script tag for an IIFE bundle.
198
243
  return html`<script src="${compiledScriptPath(sourceFile)}"></script>`;
199
244
  }
200
245
 
@@ -202,10 +247,32 @@ export function compiledStylesheetTag(sourceFile: string): HtmlSafeString {
202
247
  return html`<link rel="stylesheet" href="${compiledStylesheetPath(sourceFile)}" />`;
203
248
  }
204
249
 
205
- async function buildAssets(sourceDirectory: string, buildDirectory: string) {
250
+ export function compiledScriptModuleTag(sourceFile: string): HtmlSafeString {
251
+ // Creates a module script tag for an ESM bundle.
252
+ return html`<script type="module" src="${compiledScriptPath(sourceFile)}"></script>`;
253
+ }
254
+
255
+ export function compiledScriptPreloadPaths(sourceFile: string): string[] {
256
+ assertConfigured();
257
+
258
+ // In dev mode, we don't have a manifest, so we can't preload anything.
259
+ if (options.dev) return [];
260
+
261
+ const manifest = readManifest();
262
+ const asset = manifest[`scripts/${sourceFile}`];
263
+ if (!asset) {
264
+ throw new Error(`Unknown script asset: ${sourceFile}`);
265
+ }
266
+
267
+ return asset.preloads.map((preload) => options.publicPath + preload);
268
+ }
269
+
270
+ async function buildAssets(sourceDirectory: string, buildDirectory: string): Promise<Metafile> {
206
271
  await fs.ensureDir(buildDirectory);
207
272
 
208
- const files = await globby(path.join(sourceDirectory, '*/*.{js,jsx,ts,tsx,css}'));
273
+ const scriptFiles = await globby(path.join(sourceDirectory, 'scripts', '*.{js,jsx,ts,tsx}'));
274
+ const styleFiles = await globby(path.join(sourceDirectory, 'stylesheets', '*.css'));
275
+ const files = [...scriptFiles, ...styleFiles];
209
276
  const buildResult = await esbuild.build({
210
277
  entryPoints: files,
211
278
  target: 'es2017',
@@ -220,25 +287,74 @@ async function buildAssets(sourceDirectory: string, buildDirectory: string) {
220
287
  entryNames: '[dir]/[name]-[hash]',
221
288
  outbase: sourceDirectory,
222
289
  outdir: buildDirectory,
290
+ metafile: true, // Write metadata about the build
291
+ });
292
+
293
+ // For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)
294
+ // Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,
295
+ // which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.
296
+ const scriptBundleFiles = await globby(
297
+ path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'),
298
+ );
299
+ const esmBundleBuildResult = await esbuild.build({
300
+ entryPoints: scriptBundleFiles,
301
+ target: 'es2017',
302
+ format: 'esm',
303
+ sourcemap: 'linked',
304
+ bundle: true,
305
+ splitting: true,
306
+ minify: true,
307
+ entryNames: '[dir]/[name]-[hash]',
308
+ outbase: sourceDirectory,
309
+ outdir: buildDirectory,
223
310
  metafile: true,
224
311
  });
225
312
 
226
- return buildResult.metafile;
313
+ // Merge the resulting metafiles.
314
+ const metafile: Metafile = {
315
+ inputs: { ...buildResult.metafile.inputs, ...esmBundleBuildResult.metafile.inputs },
316
+ outputs: { ...buildResult.metafile.outputs, ...esmBundleBuildResult.metafile.outputs },
317
+ };
318
+
319
+ return metafile;
227
320
  }
228
321
 
229
322
  function makeManifest(
230
323
  metafile: Metafile,
231
324
  sourceDirectory: string,
232
325
  buildDirectory: string,
233
- ): Record<string, string> {
234
- const manifest: Record<string, string> = {};
326
+ ): AssetsManifest {
327
+ const manifest: AssetsManifest = {};
328
+
235
329
  Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
236
330
  if (!meta.entryPoint) return;
237
331
 
332
+ // Compute all the necessary preloads for each entrypoint. This includes
333
+ // any code-split chunks, as well as any files that are dynamically imported.
334
+ const preloads = new Set<string>();
335
+
336
+ // Recursively walk the `imports` dependency tree
337
+ const visit = (entry: (typeof meta)['imports'][number]) => {
338
+ if (!['import-statement', 'dynamic-import'].includes(entry.kind)) return;
339
+ if (preloads.has(entry.path)) return;
340
+ preloads.add(entry.path);
341
+ for (const imp of metafile.inputs[entry.path]?.imports ?? []) {
342
+ visit(imp);
343
+ }
344
+ };
345
+
346
+ for (const imp of meta.imports) {
347
+ visit(imp);
348
+ }
349
+
238
350
  const entryPath = path.relative(sourceDirectory, meta.entryPoint);
239
351
  const assetPath = path.relative(buildDirectory, outputPath);
240
- manifest[entryPath] = assetPath;
352
+ manifest[entryPath] = {
353
+ assetPath,
354
+ preloads: [...preloads],
355
+ };
241
356
  });
357
+
242
358
  return manifest;
243
359
  }
244
360