@prairielearn/compiled-assets 3.3.1 → 3.3.3

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,19 @@
1
1
  # @prairielearn/compiled-assets
2
2
 
3
+ ## 3.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 0900843: Switch to the `tsgo` compiler
8
+ - Updated dependencies [0900843]
9
+ - @prairielearn/html@4.0.24
10
+
11
+ ## 3.3.2
12
+
13
+ ### Patch Changes
14
+
15
+ - efe7435: Add Total size columns to build stats that sum entry point sizes with their code-split chunks
16
+
3
17
  ## 3.3.1
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -29,7 +29,7 @@ assets
29
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.
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 React components.
33
33
 
34
34
  ```text
35
35
  assets
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"","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\ninterface AssetSizes {\n raw: number;\n gzip: number;\n brotli: number;\n}\n\ntype CompressedSizes = Record<string, AssetSizes>;\n\n/**\n * Collects all unique asset paths from the manifest, including entry points and their preloads.\n */\nfunction getAllAssetPaths(manifest: AssetsManifest): Set<string> {\n const allPaths = new Set<string>();\n for (const asset of Object.values(manifest)) {\n allPaths.add(asset.assetPath);\n for (const preload of asset.preloads) {\n allPaths.add(preload);\n }\n }\n return allPaths;\n}\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 const allAssetPaths = getAllAssetPaths(manifest);\n\n await Promise.all(\n [...allAssetPaths].map(async (assetPath) => {\n const destinationFilePath = path.resolve(destination, 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[assetPath] = {\n raw: contents.length,\n gzip: gzipCompressed.length,\n brotli: brotliCompressed.length,\n };\n }),\n );\n return compressedSizes;\n}\n\n/**\n * Calculates the total size of an entry point including all its preloaded chunks.\n */\nfunction calculateTotalSizes(\n asset: AssetsManifest[string],\n compressedSizes: CompressedSizes,\n): AssetSizes {\n const entrySizes = compressedSizes[asset.assetPath];\n const total: AssetSizes = {\n raw: entrySizes.raw,\n gzip: entrySizes.gzip,\n brotli: entrySizes.brotli,\n };\n\n for (const preload of asset.preloads) {\n const preloadSizes = compressedSizes[preload];\n if (preloadSizes) {\n total.raw += preloadSizes.raw;\n total.gzip += preloadSizes.gzip;\n total.brotli += preloadSizes.brotli;\n }\n }\n\n return total;\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 totalSizes = calculateTotalSizes(asset, compressedSizes);\n\n results[entryPoint] = {\n 'Output file': asset.assetPath,\n Size: prettyBytes(totalSizes.raw),\n 'Size (gzip)': prettyBytes(totalSizes.gzip),\n 'Size (brotli)': prettyBytes(totalSizes.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/cli.js CHANGED
@@ -8,6 +8,19 @@ 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
+ * Collects all unique asset paths from the manifest, including entry points and their preloads.
13
+ */
14
+ function getAllAssetPaths(manifest) {
15
+ const allPaths = new Set();
16
+ for (const asset of Object.values(manifest)) {
17
+ allPaths.add(asset.assetPath);
18
+ for (const preload of asset.preloads) {
19
+ allPaths.add(preload);
20
+ }
21
+ }
22
+ return allPaths;
23
+ }
11
24
  /**
12
25
  * Writes gzip and brotli compressed versions of the assets in the specified directory.
13
26
  * It reads each asset file, compresses it using gzip and brotli algorithms, and writes the compressed files
@@ -19,14 +32,15 @@ const brotli = promisify(zlib.brotliCompress);
19
32
  */
20
33
  async function writeCompressedAssets(destination, manifest) {
21
34
  const compressedSizes = {};
22
- await Promise.all(Object.values(manifest).map(async (asset) => {
23
- const destinationFilePath = path.resolve(destination, asset.assetPath);
35
+ const allAssetPaths = getAllAssetPaths(manifest);
36
+ await Promise.all([...allAssetPaths].map(async (assetPath) => {
37
+ const destinationFilePath = path.resolve(destination, assetPath);
24
38
  const contents = await fs.readFile(destinationFilePath);
25
39
  const gzipCompressed = await gzip(contents);
26
40
  const brotliCompressed = await brotli(contents);
27
41
  await fs.writeFile(`${destinationFilePath}.gz`, gzipCompressed);
28
42
  await fs.writeFile(`${destinationFilePath}.br`, brotliCompressed);
29
- compressedSizes[asset.assetPath] = {
43
+ compressedSizes[assetPath] = {
30
44
  raw: contents.length,
31
45
  gzip: gzipCompressed.length,
32
46
  brotli: brotliCompressed.length,
@@ -34,6 +48,26 @@ async function writeCompressedAssets(destination, manifest) {
34
48
  }));
35
49
  return compressedSizes;
36
50
  }
51
+ /**
52
+ * Calculates the total size of an entry point including all its preloaded chunks.
53
+ */
54
+ function calculateTotalSizes(asset, compressedSizes) {
55
+ const entrySizes = compressedSizes[asset.assetPath];
56
+ const total = {
57
+ raw: entrySizes.raw,
58
+ gzip: entrySizes.gzip,
59
+ brotli: entrySizes.brotli,
60
+ };
61
+ for (const preload of asset.preloads) {
62
+ const preloadSizes = compressedSizes[preload];
63
+ if (preloadSizes) {
64
+ total.raw += preloadSizes.raw;
65
+ total.gzip += preloadSizes.gzip;
66
+ total.brotli += preloadSizes.brotli;
67
+ }
68
+ }
69
+ return total;
70
+ }
37
71
  program.command('build <source> <destination>').action(async (source, destination) => {
38
72
  const manifest = await build(source, destination);
39
73
  // Write gzip and brotli versions of the output files. Record size information
@@ -42,12 +76,12 @@ program.command('build <source> <destination>').action(async (source, destinatio
42
76
  // Format the output into an object that we can pass to `console.table`.
43
77
  const results = {};
44
78
  Object.entries(manifest).forEach(([entryPoint, asset]) => {
45
- const sizes = compressedSizes[asset.assetPath];
79
+ const totalSizes = calculateTotalSizes(asset, compressedSizes);
46
80
  results[entryPoint] = {
47
81
  'Output file': asset.assetPath,
48
- Size: prettyBytes(sizes.raw),
49
- 'Size (gzip)': prettyBytes(sizes.gzip),
50
- 'Size (brotli)': prettyBytes(sizes.brotli),
82
+ Size: prettyBytes(totalSizes.raw),
83
+ 'Size (gzip)': prettyBytes(totalSizes.gzip),
84
+ 'Size (brotli)': prettyBytes(totalSizes.brotli),
51
85
  };
52
86
  });
53
87
  console.table(results);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
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"]}
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\ninterface AssetSizes {\n raw: number;\n gzip: number;\n brotli: number;\n}\n\ntype CompressedSizes = Record<string, AssetSizes>;\n\n/**\n * Collects all unique asset paths from the manifest, including entry points and their preloads.\n */\nfunction getAllAssetPaths(manifest: AssetsManifest): Set<string> {\n const allPaths = new Set<string>();\n for (const asset of Object.values(manifest)) {\n allPaths.add(asset.assetPath);\n for (const preload of asset.preloads) {\n allPaths.add(preload);\n }\n }\n return allPaths;\n}\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 const allAssetPaths = getAllAssetPaths(manifest);\n\n await Promise.all(\n [...allAssetPaths].map(async (assetPath) => {\n const destinationFilePath = path.resolve(destination, 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[assetPath] = {\n raw: contents.length,\n gzip: gzipCompressed.length,\n brotli: brotliCompressed.length,\n };\n }),\n );\n return compressedSizes;\n}\n\n/**\n * Calculates the total size of an entry point including all its preloaded chunks.\n */\nfunction calculateTotalSizes(\n asset: AssetsManifest[string],\n compressedSizes: CompressedSizes,\n): AssetSizes {\n const entrySizes = compressedSizes[asset.assetPath];\n const total: AssetSizes = {\n raw: entrySizes.raw,\n gzip: entrySizes.gzip,\n brotli: entrySizes.brotli,\n };\n\n for (const preload of asset.preloads) {\n const preloadSizes = compressedSizes[preload];\n if (preloadSizes) {\n total.raw += preloadSizes.raw;\n total.gzip += preloadSizes.gzip;\n total.brotli += preloadSizes.brotli;\n }\n }\n\n return total;\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 totalSizes = calculateTotalSizes(asset, compressedSizes);\n\n results[entryPoint] = {\n 'Output file': asset.assetPath,\n Size: prettyBytes(totalSizes.raw),\n 'Size (gzip)': prettyBytes(totalSizes.gzip),\n 'Size (brotli)': prettyBytes(totalSizes.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
@@ -24,7 +24,7 @@ export declare function init(newOptions: Partial<CompiledAssetsOptions>): Promis
24
24
  */
25
25
  export declare function close(): Promise<void>;
26
26
  export declare function assertConfigured(): void;
27
- export declare function handler(): ((req: any, res: any, next: any) => any) | ((req: IncomingMessage, res: ServerResponse) => void);
27
+ export declare function handler(): ((req: any, res: any, next: any) => any) | ((req: IncomingMessage, res: ServerResponse<IncomingMessage>) => void);
28
28
  export declare function compiledScriptPath(sourceFile: string): string;
29
29
  export declare function compiledStylesheetPath(sourceFile: string): string;
30
30
  export declare function compiledScriptTag(sourceFile: string): HtmlSafeString;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAa,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAQ5E,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,oBAAoB,CAAC;AAS/D,MAAM,MAAM,cAAc,GAAG,MAAM,CACjC,MAAM,EACN;IACE,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CACF,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,gCAAgC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAYD,wBAAsB,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAwEpF;AAED;;GAEG;AACH,wBAAsB,KAAK,kBAG1B;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC;AAED,wBAAgB,OAAO,sDA6BC,eAAe,OAAO,cAAc,WAkC3D;AAwCD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAGpE;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAExE;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAG1E;AAED,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAavE;AAoGD,wBAAsB,KAAK,CACzB,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,cAAc,CAAC,CAUzB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAa,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAQ5E,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,oBAAoB,CAAC;AAS/D,MAAM,MAAM,cAAc,GAAG,MAAM,CACjC,MAAM,EACN;IACE,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CACF,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,gCAAgC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAYD,wBAAsB,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAwEpF;AAED;;GAEG;AACH,wBAAsB,KAAK,kBAG1B;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC;AAED,wBAAgB,OAAO,sHA+DtB;AAwCD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAGpE;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAExE;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAG1E;AAED,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAavE;AAoGD,wBAAsB,KAAK,CACzB,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,cAAc,CAAC,CAUzB","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 // 404 immediately if the file is not found.\n fallthrough: false,\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;\n\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. React 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 const preloadPath = path.relative(buildDirectory, entry.path);\n if (preloads.has(preloadPath)) return;\n preloads.add(preloadPath);\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/dist/index.js CHANGED
@@ -223,7 +223,7 @@ async function buildAssets(sourceDirectory, buildDirectory) {
223
223
  },
224
224
  metafile: true, // Write metadata about the build
225
225
  });
226
- // For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)
226
+ // For now, we only build ESM bundles for scripts that are split into chunks (i.e. React components)
227
227
  // Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,
228
228
  // which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.
229
229
  const scriptBundleFiles = await globby(path.join(sourceDirectory, 'scripts', 'esm-bundles', '**/*.{js,jsx,ts,tsx}'));
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\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 // 404 immediately if the file is not found.\n fallthrough: false,\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;\n\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 const preloadPath = path.relative(buildDirectory, entry.path);\n if (preloads.has(preloadPath)) return;\n preloads.add(preloadPath);\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"]}
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 // 404 immediately if the file is not found.\n fallthrough: false,\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;\n\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. React 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 const preloadPath = path.relative(buildDirectory, entry.path);\n if (preloads.has(preloadPath)) return;\n preloads.add(preloadPath);\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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"","sourcesContent":["import path from 'path';\n\nimport express from 'express';\nimport fs from 'fs-extra';\nimport getPort from 'get-port';\nimport fetch from 'node-fetch';\nimport tmp from 'tmp-promise';\nimport { afterEach, assert, describe, it } from 'vitest';\n\nimport {\n type CompiledAssetsOptions,\n build,\n close,\n compiledScriptPath,\n compiledStylesheetPath,\n handler,\n init,\n} from './index.js';\n\nasync function testProject(options: CompiledAssetsOptions) {\n await tmp.withDir(\n async (dir) => {\n // macOS does weird things with symlinks in its tmp directories. Resolve\n // the real path so that our asset-building machinery doesn't get confused.\n const tmpDir = await fs.realpath(dir.path);\n\n const scriptsRoot = path.join(tmpDir, 'assets', 'scripts');\n await fs.ensureDir(scriptsRoot);\n\n const stylesRoot = path.join(tmpDir, 'assets', 'stylesheets');\n await fs.ensureDir(stylesRoot);\n\n const jsScriptPath = path.join(scriptsRoot, 'foo.js');\n await fs.writeFile(jsScriptPath, 'console.log(\"foo\")');\n\n const tsScriptPath = path.join(scriptsRoot, 'bar.ts');\n await fs.writeFile(tsScriptPath, 'interface Foo {};\\n\\nconsole.log(\"bar\")');\n\n const stylesPath = path.join(stylesRoot, 'baz.css');\n await fs.writeFile(stylesPath, 'body { color: red; }');\n\n if (!options.dev) {\n await build(path.join(tmpDir, 'assets'), path.join(tmpDir, 'public', 'build'));\n }\n\n await init({\n sourceDirectory: path.join(tmpDir, 'assets'),\n buildDirectory: path.join(tmpDir, 'public', 'build'),\n publicPath: '/build',\n ...options,\n });\n\n const port = await getPort();\n const app = express();\n app.use('/build', handler());\n const server = app.listen(port);\n\n try {\n const jsRes = await fetch(`http://localhost:${port}${compiledScriptPath('foo.js')}`);\n assert.isTrue(jsRes.ok);\n assert.match(await jsRes.text(), /console\\.log\\(\"foo\"\\)/);\n\n const cssRes = await fetch(`http://localhost:${port}${compiledStylesheetPath('baz.css')}`);\n assert.isTrue(cssRes.ok);\n const cssText = await cssRes.text();\n assert.match(cssText, /body\\s*\\{/);\n assert.match(cssText, /color:\\s*red/);\n\n assert.throws(\n () => compiledScriptPath('nonexistent.js'),\n 'Unknown scripts asset: nonexistent.js',\n );\n assert.throws(\n () => compiledStylesheetPath('nonexistent.css'),\n 'Unknown stylesheets asset: nonexistent.css',\n );\n } finally {\n server.close();\n }\n },\n {\n unsafeCleanup: true,\n },\n );\n}\n\ndescribe('compiled-assets', () => {\n afterEach(async () => close());\n\n it('works in dev mode', async () => {\n await testProject({ dev: true });\n });\n\n it('works in prod mode', async () => {\n await testProject({ dev: false });\n });\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'path';\n\nimport express from 'express';\nimport fs from 'fs-extra';\nimport getPort from 'get-port';\nimport fetch from 'node-fetch';\nimport tmp from 'tmp-promise';\nimport { afterEach, assert, describe, it } from 'vitest';\n\nimport {\n type CompiledAssetsOptions,\n build,\n close,\n compiledScriptPath,\n compiledStylesheetPath,\n handler,\n init,\n} from './index.js';\n\nasync function testProject(options: CompiledAssetsOptions) {\n await tmp.withDir(\n async (dir) => {\n // macOS does weird things with symlinks in its tmp directories. Resolve\n // the real path so that our asset-building machinery doesn't get confused.\n const tmpDir = await fs.realpath(dir.path);\n\n const scriptsRoot = path.join(tmpDir, 'assets', 'scripts');\n await fs.ensureDir(scriptsRoot);\n\n const stylesRoot = path.join(tmpDir, 'assets', 'stylesheets');\n await fs.ensureDir(stylesRoot);\n\n const jsScriptPath = path.join(scriptsRoot, 'foo.js');\n await fs.writeFile(jsScriptPath, 'console.log(\"foo\")');\n\n const tsScriptPath = path.join(scriptsRoot, 'bar.ts');\n await fs.writeFile(tsScriptPath, 'interface Foo {};\\n\\nconsole.log(\"bar\")');\n\n const stylesPath = path.join(stylesRoot, 'baz.css');\n await fs.writeFile(stylesPath, 'body { color: red; }');\n\n if (!options.dev) {\n await build(path.join(tmpDir, 'assets'), path.join(tmpDir, 'public', 'build'));\n }\n\n await init({\n sourceDirectory: path.join(tmpDir, 'assets'),\n buildDirectory: path.join(tmpDir, 'public', 'build'),\n publicPath: '/build',\n ...options,\n });\n\n const port = await getPort();\n const app = express();\n app.use('/build', handler());\n const server = app.listen(port);\n\n try {\n const jsRes = await fetch(`http://localhost:${port}${compiledScriptPath('foo.js')}`);\n assert.isTrue(jsRes.ok);\n assert.match(await jsRes.text(), /console\\.log\\(\"foo\"\\)/);\n\n const cssRes = await fetch(`http://localhost:${port}${compiledStylesheetPath('baz.css')}`);\n assert.isTrue(cssRes.ok);\n const cssText = await cssRes.text();\n assert.match(cssText, /body\\s*\\{/);\n assert.match(cssText, /color:\\s*red/);\n\n assert.throws(\n () => compiledScriptPath('nonexistent.js'),\n 'Unknown scripts asset: nonexistent.js',\n );\n assert.throws(\n () => compiledStylesheetPath('nonexistent.css'),\n 'Unknown stylesheets asset: nonexistent.css',\n );\n } finally {\n server.close();\n }\n },\n {\n unsafeCleanup: true,\n },\n );\n}\n\ndescribe('compiled-assets', () => {\n afterEach(async () => close());\n\n it('works in dev mode', async () => {\n await testProject({ dev: true });\n });\n\n it('works in prod mode', async () => {\n await testProject({ dev: false });\n });\n});\n"]}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'path';\n\nimport express from 'express';\nimport fs from 'fs-extra';\nimport getPort from 'get-port';\nimport fetch from 'node-fetch';\nimport tmp from 'tmp-promise';\nimport { afterEach, assert, describe, it } from 'vitest';\n\nimport {\n type CompiledAssetsOptions,\n build,\n close,\n compiledScriptPath,\n compiledStylesheetPath,\n handler,\n init,\n} from './index.js';\n\nasync function testProject(options: CompiledAssetsOptions) {\n await tmp.withDir(\n async (dir) => {\n // macOS does weird things with symlinks in its tmp directories. Resolve\n // the real path so that our asset-building machinery doesn't get confused.\n const tmpDir = await fs.realpath(dir.path);\n\n const scriptsRoot = path.join(tmpDir, 'assets', 'scripts');\n await fs.ensureDir(scriptsRoot);\n\n const stylesRoot = path.join(tmpDir, 'assets', 'stylesheets');\n await fs.ensureDir(stylesRoot);\n\n const jsScriptPath = path.join(scriptsRoot, 'foo.js');\n await fs.writeFile(jsScriptPath, 'console.log(\"foo\")');\n\n const tsScriptPath = path.join(scriptsRoot, 'bar.ts');\n await fs.writeFile(tsScriptPath, 'interface Foo {};\\n\\nconsole.log(\"bar\")');\n\n const stylesPath = path.join(stylesRoot, 'baz.css');\n await fs.writeFile(stylesPath, 'body { color: red; }');\n\n if (!options.dev) {\n await build(path.join(tmpDir, 'assets'), path.join(tmpDir, 'public', 'build'));\n }\n\n await init({\n sourceDirectory: path.join(tmpDir, 'assets'),\n buildDirectory: path.join(tmpDir, 'public', 'build'),\n publicPath: '/build',\n ...options,\n });\n\n const port = await getPort();\n const app = express();\n app.use('/build', handler());\n const server = app.listen(port);\n\n try {\n const jsRes = await fetch(`http://localhost:${port}${compiledScriptPath('foo.js')}`);\n assert.isTrue(jsRes.ok);\n assert.match(await jsRes.text(), /console\\.log\\(\"foo\"\\)/);\n\n const cssRes = await fetch(`http://localhost:${port}${compiledStylesheetPath('baz.css')}`);\n assert.isTrue(cssRes.ok);\n const cssText = await cssRes.text();\n assert.match(cssText, /body\\s*\\{/);\n assert.match(cssText, /color:\\s*red/);\n\n assert.throws(\n () => compiledScriptPath('nonexistent.js'),\n 'Unknown scripts asset: nonexistent.js',\n );\n assert.throws(\n () => compiledStylesheetPath('nonexistent.css'),\n 'Unknown stylesheets asset: nonexistent.css',\n );\n } finally {\n server.close();\n }\n },\n {\n unsafeCleanup: true,\n },\n );\n}\n\ndescribe('compiled-assets', () => {\n afterEach(async () => close());\n\n it('works in dev mode', async () => {\n await testProject({ dev: true });\n });\n\n it('works in prod mode', async () => {\n await testProject({ dev: false });\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/compiled-assets",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,12 +10,12 @@
10
10
  "bin": "dist/cli.js",
11
11
  "main": "dist/index.js",
12
12
  "scripts": {
13
- "build": "tsc",
14
- "dev": "tsc --watch --preserveWatchOutput",
13
+ "build": "tsgo",
14
+ "dev": "tsgo --watch --preserveWatchOutput",
15
15
  "test": "vitest run --coverage"
16
16
  },
17
17
  "dependencies": {
18
- "@prairielearn/html": "^4.0.23",
18
+ "@prairielearn/html": "^4.0.24",
19
19
  "commander": "^14.0.2",
20
20
  "esbuild": "^0.27.2",
21
21
  "express-static-gzip": "^2.2.0",
@@ -26,13 +26,14 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@prairielearn/tsconfig": "^0.0.0",
29
- "@types/node": "^22.19.3",
30
- "@vitest/coverage-v8": "^4.0.16",
29
+ "@types/node": "^22.19.5",
30
+ "@typescript/native-preview": "^7.0.0-dev.20260106.1",
31
+ "@vitest/coverage-v8": "^4.0.17",
31
32
  "express": "^4.22.1",
32
33
  "get-port": "^7.1.0",
33
34
  "node-fetch": "^3.3.2",
34
35
  "tsx": "^4.21.0",
35
36
  "typescript": "^5.9.3",
36
- "vitest": "^4.0.16"
37
+ "vitest": "^4.0.17"
37
38
  }
38
39
  }
package/src/cli.ts CHANGED
@@ -12,7 +12,27 @@ import { type AssetsManifest, build } from './index.js';
12
12
  const gzip = promisify(zlib.gzip);
13
13
  const brotli = promisify(zlib.brotliCompress);
14
14
 
15
- type CompressedSizes = Record<string, Record<string, number>>;
15
+ interface AssetSizes {
16
+ raw: number;
17
+ gzip: number;
18
+ brotli: number;
19
+ }
20
+
21
+ type CompressedSizes = Record<string, AssetSizes>;
22
+
23
+ /**
24
+ * Collects all unique asset paths from the manifest, including entry points and their preloads.
25
+ */
26
+ function getAllAssetPaths(manifest: AssetsManifest): Set<string> {
27
+ const allPaths = new Set<string>();
28
+ for (const asset of Object.values(manifest)) {
29
+ allPaths.add(asset.assetPath);
30
+ for (const preload of asset.preloads) {
31
+ allPaths.add(preload);
32
+ }
33
+ }
34
+ return allPaths;
35
+ }
16
36
 
17
37
  /**
18
38
  * Writes gzip and brotli compressed versions of the assets in the specified directory.
@@ -28,15 +48,17 @@ async function writeCompressedAssets(
28
48
  manifest: AssetsManifest,
29
49
  ): Promise<CompressedSizes> {
30
50
  const compressedSizes: CompressedSizes = {};
51
+ const allAssetPaths = getAllAssetPaths(manifest);
52
+
31
53
  await Promise.all(
32
- Object.values(manifest).map(async (asset) => {
33
- const destinationFilePath = path.resolve(destination, asset.assetPath);
54
+ [...allAssetPaths].map(async (assetPath) => {
55
+ const destinationFilePath = path.resolve(destination, assetPath);
34
56
  const contents = await fs.readFile(destinationFilePath);
35
57
  const gzipCompressed = await gzip(contents);
36
58
  const brotliCompressed = await brotli(contents);
37
59
  await fs.writeFile(`${destinationFilePath}.gz`, gzipCompressed);
38
60
  await fs.writeFile(`${destinationFilePath}.br`, brotliCompressed);
39
- compressedSizes[asset.assetPath] = {
61
+ compressedSizes[assetPath] = {
40
62
  raw: contents.length,
41
63
  gzip: gzipCompressed.length,
42
64
  brotli: brotliCompressed.length,
@@ -46,6 +68,32 @@ async function writeCompressedAssets(
46
68
  return compressedSizes;
47
69
  }
48
70
 
71
+ /**
72
+ * Calculates the total size of an entry point including all its preloaded chunks.
73
+ */
74
+ function calculateTotalSizes(
75
+ asset: AssetsManifest[string],
76
+ compressedSizes: CompressedSizes,
77
+ ): AssetSizes {
78
+ const entrySizes = compressedSizes[asset.assetPath];
79
+ const total: AssetSizes = {
80
+ raw: entrySizes.raw,
81
+ gzip: entrySizes.gzip,
82
+ brotli: entrySizes.brotli,
83
+ };
84
+
85
+ for (const preload of asset.preloads) {
86
+ const preloadSizes = compressedSizes[preload];
87
+ if (preloadSizes) {
88
+ total.raw += preloadSizes.raw;
89
+ total.gzip += preloadSizes.gzip;
90
+ total.brotli += preloadSizes.brotli;
91
+ }
92
+ }
93
+
94
+ return total;
95
+ }
96
+
49
97
  program.command('build <source> <destination>').action(async (source, destination) => {
50
98
  const manifest = await build(source, destination);
51
99
 
@@ -56,12 +104,13 @@ program.command('build <source> <destination>').action(async (source, destinatio
56
104
  // Format the output into an object that we can pass to `console.table`.
57
105
  const results: Record<string, any> = {};
58
106
  Object.entries(manifest).forEach(([entryPoint, asset]) => {
59
- const sizes = compressedSizes[asset.assetPath];
107
+ const totalSizes = calculateTotalSizes(asset, compressedSizes);
108
+
60
109
  results[entryPoint] = {
61
110
  'Output file': asset.assetPath,
62
- Size: prettyBytes(sizes.raw),
63
- 'Size (gzip)': prettyBytes(sizes.gzip),
64
- 'Size (brotli)': prettyBytes(sizes.brotli),
111
+ Size: prettyBytes(totalSizes.raw),
112
+ 'Size (gzip)': prettyBytes(totalSizes.gzip),
113
+ 'Size (brotli)': prettyBytes(totalSizes.brotli),
65
114
  };
66
115
  });
67
116
  console.table(results);
package/src/index.ts CHANGED
@@ -302,7 +302,7 @@ async function buildAssets(sourceDirectory: string, buildDirectory: string): Pro
302
302
  metafile: true, // Write metadata about the build
303
303
  });
304
304
 
305
- // For now, we only build ESM bundles for scripts that are split into chunks (i.e. Preact components)
305
+ // For now, we only build ESM bundles for scripts that are split into chunks (i.e. React components)
306
306
  // Using 'type=module' in the script tag for ESM means that it is loaded after all 'classic' scripts,
307
307
  // which causes issues with bootstrap-table. See https://github.com/PrairieLearn/PrairieLearn/pull/12180.
308
308
  const scriptBundleFiles = await globby(