@prairielearn/compiled-assets 1.0.2 → 2.0.1

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
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 8fd47d928: Upgrade all dependencies
8
+ - Updated dependencies [8fd47d928]
9
+ - @prairielearn/html@3.0.1
10
+
11
+ ## 2.0.0
12
+
13
+ ### Major Changes
14
+
15
+ - f71a12d41: Add support for stylesheets; use esbuild asset server in dev mode
16
+
3
17
  ## 1.0.2
4
18
 
5
19
  ### Patch Changes
package/dist/cli.js CHANGED
@@ -13,32 +13,37 @@ const commander_1 = require("commander");
13
13
  const index_js_1 = require("./index.js");
14
14
  const gzip = (0, util_1.promisify)(zlib_1.default.gzip);
15
15
  const brotli = (0, util_1.promisify)(zlib_1.default.brotliCompress);
16
- commander_1.program.command('build <source> <destination>').action(async (source, destination) => {
17
- const metafile = await (0, index_js_1.build)(source, destination);
18
- // Write gzip and brotli versions of the output files. Record size information
19
- // so we can show it to the user.
16
+ async function writeCompressedAssets(destination, manifest) {
20
17
  const compressedSizes = {};
21
- await Promise.all(Object.keys(metafile.outputs).map(async (outputPath) => {
22
- const contents = await fs_extra_1.default.readFile(outputPath);
18
+ await Promise.all(Object.values(manifest).map(async (filePath) => {
19
+ const destinationFilePath = path_1.default.resolve(destination, filePath);
20
+ const contents = await fs_extra_1.default.readFile(destinationFilePath);
23
21
  const gzipCompressed = await gzip(contents);
24
22
  const brotliCompressed = await brotli(contents);
25
- await fs_extra_1.default.writeFile(`${outputPath}.gz`, gzipCompressed);
26
- await fs_extra_1.default.writeFile(`${outputPath}.br`, brotliCompressed);
27
- compressedSizes[outputPath] = {
23
+ await fs_extra_1.default.writeFile(`${destinationFilePath}.gz`, gzipCompressed);
24
+ await fs_extra_1.default.writeFile(`${destinationFilePath}.br`, brotliCompressed);
25
+ compressedSizes[filePath] = {
26
+ raw: contents.length,
28
27
  gzip: gzipCompressed.length,
29
28
  brotli: brotliCompressed.length,
30
29
  };
31
30
  }));
31
+ return compressedSizes;
32
+ }
33
+ commander_1.program.command('build <source> <destination>').action(async (source, destination) => {
34
+ const manifest = await (0, index_js_1.build)(source, destination);
35
+ // Write gzip and brotli versions of the output files. Record size information
36
+ // so we can show it to the user.
37
+ const compressedSizes = await writeCompressedAssets(destination, manifest);
32
38
  // Format the output into an object that we can pass to `console.table`.
33
39
  const results = {};
34
- Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
35
- if (!meta.entryPoint)
36
- return;
37
- results[path_1.default.basename(meta.entryPoint)] = {
38
- 'Output file': path_1.default.basename(outputPath),
39
- Size: (0, pretty_bytes_1.default)(meta.bytes),
40
- 'Size (gzip)': (0, pretty_bytes_1.default)(compressedSizes[outputPath].gzip),
41
- 'Size (brotli)': (0, pretty_bytes_1.default)(compressedSizes[outputPath].brotli),
40
+ Object.entries(manifest).forEach(([entryPoint, assetPath]) => {
41
+ const sizes = compressedSizes[assetPath];
42
+ results[entryPoint] = {
43
+ 'Output file': assetPath,
44
+ Size: (0, pretty_bytes_1.default)(sizes.raw),
45
+ 'Size (gzip)': (0, pretty_bytes_1.default)(sizes.gzip),
46
+ 'Size (brotli)': (0, pretty_bytes_1.default)(sizes.brotli),
42
47
  };
43
48
  });
44
49
  console.table(results);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;AACA,wDAA0B;AAC1B,gEAAuC;AACvC,gDAAwB;AACxB,+BAAiC;AACjC,gDAAwB;AACxB,yCAAoC;AAEpC,yCAAmC;AAEnC,MAAM,IAAI,GAAG,IAAA,gBAAS,EAAC,cAAI,CAAC,IAAI,CAAC,CAAC;AAClC,MAAM,MAAM,GAAG,IAAA,gBAAS,EAAC,cAAI,CAAC,cAAc,CAAC,CAAC;AAE9C,mBAAO,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE;IACnF,MAAM,QAAQ,GAAG,MAAM,IAAA,gBAAK,EAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAElD,8EAA8E;IAC9E,iCAAiC;IACjC,MAAM,eAAe,GAA2C,EAAE,CAAC;IACnE,MAAM,OAAO,CAAC,GAAG,CACf,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACrD,MAAM,QAAQ,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC/C,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,gBAAgB,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,kBAAE,CAAC,SAAS,CAAC,GAAG,UAAU,KAAK,EAAE,cAAc,CAAC,CAAC;QACvD,MAAM,kBAAE,CAAC,SAAS,CAAC,GAAG,UAAU,KAAK,EAAE,gBAAgB,CAAC,CAAC;QACzD,eAAe,CAAC,UAAU,CAAC,GAAG;YAC5B,IAAI,EAAE,cAAc,CAAC,MAAM;YAC3B,MAAM,EAAE,gBAAgB,CAAC,MAAM;SAChC,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IAEF,wEAAwE;IACxE,MAAM,OAAO,GAAwB,EAAE,CAAC;IACxC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,EAAE;QAC9D,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO;QAC7B,OAAO,CAAC,cAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG;YACxC,aAAa,EAAE,cAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;YACxC,IAAI,EAAE,IAAA,sBAAW,EAAC,IAAI,CAAC,KAAK,CAAC;YAC7B,aAAa,EAAE,IAAA,sBAAW,EAAC,eAAe,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YAC5D,eAAe,EAAE,IAAA,sBAAW,EAAC,eAAe,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;SACjE,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,mBAAO,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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;AACA,wDAA0B;AAC1B,gEAAuC;AACvC,gDAAwB;AACxB,+BAAiC;AACjC,gDAAwB;AACxB,yCAAoC;AAEpC,yCAAmC;AAEnC,MAAM,IAAI,GAAG,IAAA,gBAAS,EAAC,cAAI,CAAC,IAAI,CAAC,CAAC;AAClC,MAAM,MAAM,GAAG,IAAA,gBAAS,EAAC,cAAI,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,cAAI,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QAChE,MAAM,QAAQ,GAAG,MAAM,kBAAE,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,kBAAE,CAAC,SAAS,CAAC,GAAG,mBAAmB,KAAK,EAAE,cAAc,CAAC,CAAC;QAChE,MAAM,kBAAE,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,mBAAO,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE;IACnF,MAAM,QAAQ,GAAG,MAAM,IAAA,gBAAK,EAAC,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,IAAA,sBAAW,EAAC,KAAK,CAAC,GAAG,CAAC;YAC5B,aAAa,EAAE,IAAA,sBAAW,EAAC,KAAK,CAAC,IAAI,CAAC;YACtC,eAAe,EAAE,IAAA,sBAAW,EAAC,KAAK,CAAC,MAAM,CAAC;SAC3C,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,mBAAO,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"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { RequestHandler } from 'express';
2
- import esbuild from 'esbuild';
3
2
  import { HtmlSafeString } from '@prairielearn/html';
3
+ type AssetsManifest = Record<string, string>;
4
4
  export interface CompiledAssetsOptions {
5
5
  /**
6
- * Whether the app is running in dev mode. If dev modde is enabled, then
6
+ * Whether the app is running in dev mode. If dev mode is enabled, then
7
7
  * assets will be built on the fly as they're requested. Otherwise, assets
8
8
  * should have been pre-compiled to the `buildDirectory` directory.
9
9
  */
@@ -15,9 +15,16 @@ export interface CompiledAssetsOptions {
15
15
  /** The path that assets will be served from, e.g. `/build/`. */
16
16
  publicPath?: string;
17
17
  }
18
- export declare function init(newOptions: Partial<CompiledAssetsOptions>): void;
18
+ export declare function init(newOptions: Partial<CompiledAssetsOptions>): Promise<void>;
19
+ /**
20
+ * Shuts down the development assets compiler if it is running.
21
+ */
22
+ export declare function close(): Promise<void>;
19
23
  export declare function assertConfigured(): void;
20
24
  export declare function handler(): RequestHandler;
21
25
  export declare function compiledScriptPath(sourceFile: string): string;
26
+ export declare function compiledStylesheetPath(sourceFile: string): string;
22
27
  export declare function compiledScriptTag(sourceFile: string): HtmlSafeString;
23
- export declare function build(sourceDirectory: string, buildDirectory: string): Promise<esbuild.Metafile>;
28
+ export declare function compiledStylesheetTag(sourceFile: string): HtmlSafeString;
29
+ export declare function build(sourceDirectory: string, buildDirectory: string): Promise<AssetsManifest>;
30
+ export {};
package/dist/index.js CHANGED
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.build = exports.compiledScriptTag = exports.compiledScriptPath = exports.handler = exports.assertConfigured = exports.init = void 0;
6
+ exports.build = exports.compiledStylesheetTag = exports.compiledScriptTag = exports.compiledStylesheetPath = exports.compiledScriptPath = exports.handler = exports.assertConfigured = exports.close = exports.init = void 0;
7
7
  const express_static_gzip_1 = __importDefault(require("express-static-gzip"));
8
8
  const esbuild_1 = __importDefault(require("esbuild"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const globby_1 = __importDefault(require("globby"));
11
11
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const node_http_1 = __importDefault(require("node:http"));
12
13
  const html_1 = require("@prairielearn/html");
13
14
  const DEFAULT_OPTIONS = {
14
15
  dev: process.env.NODE_ENV !== 'production',
@@ -17,7 +18,9 @@ const DEFAULT_OPTIONS = {
17
18
  publicPath: '/build/',
18
19
  };
19
20
  let options = { ...DEFAULT_OPTIONS };
20
- function init(newOptions) {
21
+ let esbuildContext = null;
22
+ let esbuildServer = null;
23
+ async function init(newOptions) {
21
24
  options = {
22
25
  ...DEFAULT_OPTIONS,
23
26
  ...newOptions,
@@ -25,8 +28,39 @@ function init(newOptions) {
25
28
  if (!options.publicPath.endsWith('/')) {
26
29
  options.publicPath += '/';
27
30
  }
31
+ if (options.dev) {
32
+ // Use esbuild's asset server in development.
33
+ //
34
+ // Note that esbuild doesn't support globs, so the server will not pick up
35
+ // new entrypoints that are added while the server is running.
36
+ const sourceGlob = path_1.default.join(options.sourceDirectory, '*', '*.{js,ts,css}');
37
+ const sourcePaths = await (0, globby_1.default)(sourceGlob);
38
+ esbuildContext = await esbuild_1.default.context({
39
+ entryPoints: sourcePaths,
40
+ target: 'es6',
41
+ format: 'iife',
42
+ sourcemap: 'inline',
43
+ bundle: true,
44
+ write: false,
45
+ loader: {
46
+ '.woff': 'file',
47
+ '.woff2': 'file',
48
+ },
49
+ outbase: options.sourceDirectory,
50
+ outdir: options.buildDirectory,
51
+ entryNames: '[dir]/[name]',
52
+ });
53
+ esbuildServer = await esbuildContext.serve();
54
+ }
28
55
  }
29
56
  exports.init = init;
57
+ /**
58
+ * Shuts down the development assets compiler if it is running.
59
+ */
60
+ async function close() {
61
+ esbuildContext?.dispose();
62
+ }
63
+ exports.close = close;
30
64
  function assertConfigured() {
31
65
  if (!options) {
32
66
  throw new Error('@prairielearn/compiled-assets was not configured');
@@ -49,77 +83,68 @@ function handler() {
49
83
  },
50
84
  });
51
85
  }
86
+ if (!esbuildServer) {
87
+ throw new Error('esbuild server not initialized');
88
+ }
89
+ const { host, port } = esbuildServer;
52
90
  // We're running in dev mode, so we need to boot up ESBuild to start building
53
91
  // and watching our assets.
54
92
  return function (req, res) {
55
- // Strip leading slash from `req.url`.
56
- let assetPath = req.url;
57
- if (assetPath.startsWith('/')) {
58
- assetPath = assetPath.slice(1);
59
- }
60
- const resolvedSourceDirectory = path_1.default.resolve(options?.sourceDirectory);
61
- const resolvedAssetPath = path_1.default.resolve(resolvedSourceDirectory, assetPath);
62
- if (!resolvedAssetPath.startsWith(resolvedSourceDirectory)) {
63
- // Probably path traversal.
64
- res.status(404).send('Not found');
65
- return;
66
- }
67
- // esbuild should be fast enough that we can just build everything on the
68
- // fly as it's requested! This is probably just for prototyping though. We
69
- // should use some kind of caching to ensure that local dev stays fast.
70
- esbuild_1.default
71
- .build({
72
- entryPoints: [resolvedAssetPath],
73
- target: 'es6',
74
- format: 'iife',
75
- sourcemap: 'inline',
76
- bundle: true,
77
- write: false,
78
- })
79
- .then((buildResult) => {
80
- res
81
- .setHeader('Content-Type', 'application/javascript; charset=UTF-8')
82
- .status(200)
83
- .send(buildResult.outputFiles[0].text);
84
- }, (buildError) => {
85
- res.status(500).send(buildError.message);
93
+ const proxyReq = node_http_1.default.request({
94
+ hostname: host,
95
+ port,
96
+ path: req.url,
97
+ method: req.method,
98
+ headers: req.headers,
99
+ }, (proxyRes) => {
100
+ res.writeHead(proxyRes.statusCode ?? 500, proxyRes.headers);
101
+ proxyRes.pipe(res, { end: true });
86
102
  });
103
+ req.pipe(proxyReq, { end: true });
87
104
  };
88
105
  }
89
106
  exports.handler = handler;
90
- let cachedScriptsManifest = null;
91
- function readScriptsManifest() {
107
+ let cachedManifest = null;
108
+ function readManifest() {
92
109
  assertConfigured();
93
- if (!cachedScriptsManifest) {
94
- const manifestPath = path_1.default.join(options.buildDirectory, 'scripts', 'manifest.json');
95
- cachedScriptsManifest = fs_extra_1.default.readJSONSync(manifestPath);
110
+ if (!cachedManifest) {
111
+ const manifestPath = path_1.default.join(options.buildDirectory, 'manifest.json');
112
+ cachedManifest = fs_extra_1.default.readJSONSync(manifestPath);
96
113
  }
97
- return cachedScriptsManifest;
114
+ return cachedManifest;
98
115
  }
99
- function compiledScriptPath(sourceFile) {
116
+ function compiledPath(type, sourceFile) {
100
117
  assertConfigured();
118
+ const sourceFilePath = `${type}/${sourceFile}`;
101
119
  if (options.dev) {
102
- return options.publicPath + 'scripts/' + sourceFile;
120
+ return options.publicPath + sourceFilePath.replace(/\.(js|ts)x?$/, '.js');
103
121
  }
104
- const scriptsManifest = readScriptsManifest();
105
- const scriptPath = scriptsManifest[sourceFile];
106
- if (!scriptPath) {
107
- throw new Error(`Unknown script: ${sourceFile}`);
122
+ const manifest = readManifest();
123
+ const assetPath = manifest[sourceFilePath];
124
+ if (!assetPath) {
125
+ throw new Error(`Unknown ${type} asset: ${sourceFile}`);
108
126
  }
109
- return options.publicPath + 'scripts/' + scriptPath;
127
+ return `${options.publicPath}/${assetPath}`;
128
+ }
129
+ function compiledScriptPath(sourceFile) {
130
+ return compiledPath('scripts', sourceFile);
110
131
  }
111
132
  exports.compiledScriptPath = compiledScriptPath;
133
+ function compiledStylesheetPath(sourceFile) {
134
+ return compiledPath('stylesheets', sourceFile);
135
+ }
136
+ exports.compiledStylesheetPath = compiledStylesheetPath;
112
137
  function compiledScriptTag(sourceFile) {
113
138
  return (0, html_1.html) `<script src="${compiledScriptPath(sourceFile)}"></script>`;
114
139
  }
115
140
  exports.compiledScriptTag = compiledScriptTag;
116
- async function build(sourceDirectory, buildDirectory) {
117
- // Remove existing assets to ensure that no stale assets are left behind.
118
- await fs_extra_1.default.remove(buildDirectory);
119
- const scriptsSourceRoot = path_1.default.resolve(sourceDirectory, 'scripts');
120
- const scriptsBuildRoot = path_1.default.resolve(buildDirectory, 'scripts');
121
- await fs_extra_1.default.ensureDir(scriptsBuildRoot);
122
- const files = await (0, globby_1.default)(path_1.default.join(scriptsSourceRoot, '*.{js,jsx,ts,tsx}'));
141
+ function compiledStylesheetTag(sourceFile) {
142
+ return (0, html_1.html) `<link rel="stylesheet" href="${compiledStylesheetPath(sourceFile)}" />`;
143
+ }
144
+ exports.compiledStylesheetTag = compiledStylesheetTag;
145
+ async function buildAssets(sourceDirectory, buildDirectory) {
146
+ await fs_extra_1.default.ensureDir(buildDirectory);
147
+ const files = await (0, globby_1.default)(path_1.default.join(sourceDirectory, '*/*.{js,jsx,ts,tsx,css}'));
123
148
  const buildResult = await esbuild_1.default.build({
124
149
  entryPoints: files,
125
150
  target: 'es6',
@@ -127,24 +152,36 @@ async function build(sourceDirectory, buildDirectory) {
127
152
  sourcemap: 'linked',
128
153
  bundle: true,
129
154
  minify: true,
130
- entryNames: '[name]-[hash]',
131
- outdir: scriptsBuildRoot,
155
+ loader: {
156
+ '.woff': 'file',
157
+ '.woff2': 'file',
158
+ },
159
+ entryNames: '[dir]/[name]-[hash]',
160
+ outbase: sourceDirectory,
161
+ outdir: buildDirectory,
132
162
  metafile: true,
133
163
  });
134
- // Write asset manifest so that we can map from "input" names to built names
135
- // at runtime.
136
- const { metafile } = buildResult;
164
+ return buildResult.metafile;
165
+ }
166
+ function makeManifest(metafile, sourceDirectory, buildDirectory) {
137
167
  const manifest = {};
138
168
  Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
139
169
  if (!meta.entryPoint)
140
170
  return;
141
- const entryPath = path_1.default.basename(meta.entryPoint);
142
- const assetPath = path_1.default.basename(outputPath);
171
+ const entryPath = path_1.default.relative(sourceDirectory, meta.entryPoint);
172
+ const assetPath = path_1.default.relative(buildDirectory, outputPath);
143
173
  manifest[entryPath] = assetPath;
144
174
  });
145
- const manifestPath = path_1.default.join(scriptsBuildRoot, 'manifest.json');
175
+ return manifest;
176
+ }
177
+ async function build(sourceDirectory, buildDirectory) {
178
+ // Remove existing assets to ensure that no stale assets are left behind.
179
+ await fs_extra_1.default.remove(buildDirectory);
180
+ const metafile = await buildAssets(sourceDirectory, buildDirectory);
181
+ const manifest = makeManifest(metafile, sourceDirectory, buildDirectory);
182
+ const manifestPath = path_1.default.join(buildDirectory, 'manifest.json');
146
183
  await fs_extra_1.default.writeJSON(manifestPath, manifest);
147
- return metafile;
184
+ return manifest;
148
185
  }
149
186
  exports.build = build;
150
187
  //# sourceMappingURL=index.js.map
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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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -13,18 +13,25 @@ const node_fetch_1 = __importDefault(require("node-fetch"));
13
13
  const index_1 = require("./index");
14
14
  async function testProject(options) {
15
15
  await tmp_promise_1.default.withDir(async (dir) => {
16
- const scriptsRoot = path_1.default.join(dir.path, 'assets', 'scripts');
16
+ // macOS does weird things with symlinks in its tmp directories. Resolve
17
+ // the real path so that our asset-building machinery doesn't get confused.
18
+ const tmpDir = await fs_extra_1.default.realpath(dir.path);
19
+ const scriptsRoot = path_1.default.join(tmpDir, 'assets', 'scripts');
17
20
  await fs_extra_1.default.ensureDir(scriptsRoot);
21
+ const stylesRoot = path_1.default.join(tmpDir, 'assets', 'stylesheets');
22
+ await fs_extra_1.default.ensureDir(stylesRoot);
18
23
  const jsScriptPath = path_1.default.join(scriptsRoot, 'foo.js');
24
+ await fs_extra_1.default.writeFile(jsScriptPath, 'console.log("foo")');
19
25
  const tsScriptPath = path_1.default.join(scriptsRoot, 'bar.ts');
20
- fs_extra_1.default.writeFile(jsScriptPath, 'console.log("foo")');
21
- fs_extra_1.default.writeFile(tsScriptPath, 'interface Foo {};\n\nconsole.log("bar")');
26
+ await fs_extra_1.default.writeFile(tsScriptPath, 'interface Foo {};\n\nconsole.log("bar")');
27
+ const stylesPath = path_1.default.join(stylesRoot, 'baz.css');
28
+ await fs_extra_1.default.writeFile(stylesPath, 'body { color: red; }');
22
29
  if (!options.dev) {
23
- await (0, index_1.build)(path_1.default.join(dir.path, 'assets'), path_1.default.join(dir.path, 'public', 'build'));
30
+ await (0, index_1.build)(path_1.default.join(tmpDir, 'assets'), path_1.default.join(tmpDir, 'public', 'build'));
24
31
  }
25
- (0, index_1.init)({
26
- sourceDirectory: path_1.default.join(dir.path, 'assets'),
27
- buildDirectory: path_1.default.join(dir.path, 'public', 'build'),
32
+ await (0, index_1.init)({
33
+ sourceDirectory: path_1.default.join(tmpDir, 'assets'),
34
+ buildDirectory: path_1.default.join(tmpDir, 'public', 'build'),
28
35
  publicPath: '/build',
29
36
  ...options,
30
37
  });
@@ -33,9 +40,14 @@ async function testProject(options) {
33
40
  app.use('/build', (0, index_1.handler)());
34
41
  const server = app.listen(port);
35
42
  try {
36
- const res = await (0, node_fetch_1.default)(`http://localhost:${port}${(0, index_1.compiledScriptPath)('foo.js')}`);
37
- chai_1.assert.isTrue(res.ok);
38
- chai_1.assert.match(await res.text(), /console\.log\("foo"\)/);
43
+ const jsRes = await (0, node_fetch_1.default)(`http://localhost:${port}${(0, index_1.compiledScriptPath)('foo.js')}`);
44
+ chai_1.assert.isTrue(jsRes.ok);
45
+ chai_1.assert.match(await jsRes.text(), /console\.log\("foo"\)/);
46
+ const cssRes = await (0, node_fetch_1.default)(`http://localhost:${port}${(0, index_1.compiledStylesheetPath)('baz.css')}`);
47
+ chai_1.assert.isTrue(cssRes.ok);
48
+ const cssText = await cssRes.text();
49
+ chai_1.assert.match(cssText, /body\s*\{/);
50
+ chai_1.assert.match(cssText, /color:\s*red/);
39
51
  }
40
52
  finally {
41
53
  server.close();
@@ -45,6 +57,7 @@ async function testProject(options) {
45
57
  });
46
58
  }
47
59
  describe('compiled-assets', () => {
60
+ afterEach(async () => (0, index_1.close)());
48
61
  it('works in dev mode', async () => {
49
62
  await testProject({ dev: true });
50
63
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":";;;;;AAAA,8DAA8B;AAC9B,wDAA0B;AAC1B,gDAAwB;AACxB,wDAA+B;AAC/B,+BAA8B;AAC9B,sDAA8B;AAC9B,4DAA+B;AAE/B,mCAA0F;AAE1F,KAAK,UAAU,WAAW,CAAC,OAA8B;IACvD,MAAM,qBAAG,CAAC,OAAO,CACf,KAAK,EAAE,GAAG,EAAE,EAAE;QACZ,MAAM,WAAW,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;QAC7D,MAAM,kBAAE,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAEhC,MAAM,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACtD,MAAM,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACtD,kBAAE,CAAC,SAAS,CAAC,YAAY,EAAE,oBAAoB,CAAC,CAAC;QACjD,kBAAE,CAAC,SAAS,CAAC,YAAY,EAAE,yCAAyC,CAAC,CAAC;QAEtE,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YAChB,MAAM,IAAA,aAAK,EAAC,cAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,cAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;SACpF;QAED,IAAA,YAAI,EAAC;YACH,eAAe,EAAE,cAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC;YAC9C,cAAc,EAAE,cAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC;YACtD,UAAU,EAAE,QAAQ;YACpB,GAAG,OAAO;SACX,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,IAAA,kBAAO,GAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;QACtB,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAA,eAAO,GAAE,CAAC,CAAC;QAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAEhC,IAAI;YACF,MAAM,GAAG,GAAG,MAAM,IAAA,oBAAK,EAAC,oBAAoB,IAAI,GAAG,IAAA,0BAAkB,EAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACnF,aAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACtB,aAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,uBAAuB,CAAC,CAAC;SACzD;gBAAS;YACR,MAAM,CAAC,KAAK,EAAE,CAAC;SAChB;IACH,CAAC,EACD;QACE,aAAa,EAAE,IAAI;KACpB,CACF,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE;QACjC,MAAM,WAAW,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;QAClC,MAAM,WAAW,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":";;;;;AAAA,8DAA8B;AAC9B,wDAA0B;AAC1B,gDAAwB;AACxB,wDAA+B;AAC/B,+BAA8B;AAC9B,sDAA8B;AAC9B,4DAA+B;AAE/B,mCAQiB;AAEjB,KAAK,UAAU,WAAW,CAAC,OAA8B;IACvD,MAAM,qBAAG,CAAC,OAAO,CACf,KAAK,EAAE,GAAG,EAAE,EAAE;QACZ,wEAAwE;QACxE,2EAA2E;QAC3E,MAAM,MAAM,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAE3C,MAAM,WAAW,GAAG,cAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;QAC3D,MAAM,kBAAE,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAEhC,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;QAC9D,MAAM,kBAAE,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAE/B,MAAM,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACtD,MAAM,kBAAE,CAAC,SAAS,CAAC,YAAY,EAAE,oBAAoB,CAAC,CAAC;QAEvD,MAAM,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACtD,MAAM,kBAAE,CAAC,SAAS,CAAC,YAAY,EAAE,yCAAyC,CAAC,CAAC;QAE5E,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QACpD,MAAM,kBAAE,CAAC,SAAS,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAC;QAEvD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YAChB,MAAM,IAAA,aAAK,EAAC,cAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;SAChF;QAED,MAAM,IAAA,YAAI,EAAC;YACT,eAAe,EAAE,cAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;YAC5C,cAAc,EAAE,cAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC;YACpD,UAAU,EAAE,QAAQ;YACpB,GAAG,OAAO;SACX,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,IAAA,kBAAO,GAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;QACtB,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAA,eAAO,GAAE,CAAC,CAAC;QAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAEhC,IAAI;YACF,MAAM,KAAK,GAAG,MAAM,IAAA,oBAAK,EAAC,oBAAoB,IAAI,GAAG,IAAA,0BAAkB,EAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACrF,aAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACxB,aAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,EAAE,uBAAuB,CAAC,CAAC;YAE1D,MAAM,MAAM,GAAG,MAAM,IAAA,oBAAK,EAAC,oBAAoB,IAAI,GAAG,IAAA,8BAAsB,EAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YAC3F,aAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACzB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YACpC,aAAM,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACnC,aAAM,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;SACvC;gBAAS;YACR,MAAM,CAAC,KAAK,EAAE,CAAC;SAChB;IACH,CAAC,EACD;QACE,aAAa,EAAE,IAAI;KACpB,CACF,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC,IAAA,aAAK,GAAE,CAAC,CAAC;IAE/B,EAAE,CAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE;QACjC,MAAM,WAAW,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;QAClC,MAAM,WAAW,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/compiled-assets",
3
- "version": "1.0.2",
3
+ "version": "2.0.1",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/cli.js",
6
6
  "repository": {
@@ -14,9 +14,9 @@
14
14
  "test": "mocha --no-config --require ts-node/register src/index.test.ts"
15
15
  },
16
16
  "dependencies": {
17
- "@prairielearn/html": "^3.0.0",
18
- "commander": "^10.0.0",
19
- "esbuild": "^0.17.15",
17
+ "@prairielearn/html": "^3.0.1",
18
+ "commander": "^10.0.1",
19
+ "esbuild": "^0.17.19",
20
20
  "express": "^4.18.2",
21
21
  "express-static-gzip": "^2.1.7",
22
22
  "fs-extra": "^11.1.1",
@@ -25,13 +25,13 @@
25
25
  "tmp-promise": "^3.0.3"
26
26
  },
27
27
  "devDependencies": {
28
- "@prairielearn/tsconfig": "*",
29
- "@types/node": "^18.15.11",
28
+ "@prairielearn/tsconfig": "^0.0.0",
29
+ "@types/node": "^18.16.16",
30
30
  "chai": "^4.3.7",
31
31
  "get-port": "^5.1.1",
32
32
  "mocha": "^10.2.0",
33
- "node-fetch": "^2.6.9",
33
+ "node-fetch": "^2.6.11",
34
34
  "ts-node": "^10.9.1",
35
- "typescript": "^4.9.5"
35
+ "typescript": "^5.1.3"
36
36
  }
37
37
  }
package/src/cli.ts CHANGED
@@ -11,35 +11,47 @@ import { build } from './index.js';
11
11
  const gzip = promisify(zlib.gzip);
12
12
  const brotli = promisify(zlib.brotliCompress);
13
13
 
14
- program.command('build <source> <destination>').action(async (source, destination) => {
15
- const metafile = await build(source, destination);
14
+ type CompressedSizes = Record<string, Record<string, number>>;
16
15
 
17
- // Write gzip and brotli versions of the output files. Record size information
18
- // so we can show it to the user.
19
- const compressedSizes: Record<string, Record<string, number>> = {};
16
+ async function writeCompressedAssets(
17
+ destination: string,
18
+ manifest: Record<string, string>
19
+ ): Promise<CompressedSizes> {
20
+ const compressedSizes: CompressedSizes = {};
20
21
  await Promise.all(
21
- Object.keys(metafile.outputs).map(async (outputPath) => {
22
- const contents = await fs.readFile(outputPath);
22
+ Object.values(manifest).map(async (filePath) => {
23
+ const destinationFilePath = path.resolve(destination, filePath);
24
+ const contents = await fs.readFile(destinationFilePath);
23
25
  const gzipCompressed = await gzip(contents);
24
26
  const brotliCompressed = await brotli(contents);
25
- await fs.writeFile(`${outputPath}.gz`, gzipCompressed);
26
- await fs.writeFile(`${outputPath}.br`, brotliCompressed);
27
- compressedSizes[outputPath] = {
27
+ await fs.writeFile(`${destinationFilePath}.gz`, gzipCompressed);
28
+ await fs.writeFile(`${destinationFilePath}.br`, brotliCompressed);
29
+ compressedSizes[filePath] = {
30
+ raw: contents.length,
28
31
  gzip: gzipCompressed.length,
29
32
  brotli: brotliCompressed.length,
30
33
  };
31
34
  })
32
35
  );
36
+ return compressedSizes;
37
+ }
38
+
39
+ program.command('build <source> <destination>').action(async (source, destination) => {
40
+ const manifest = await build(source, destination);
41
+
42
+ // Write gzip and brotli versions of the output files. Record size information
43
+ // so we can show it to the user.
44
+ const compressedSizes = await writeCompressedAssets(destination, manifest);
33
45
 
34
46
  // Format the output into an object that we can pass to `console.table`.
35
47
  const results: Record<string, any> = {};
36
- Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
37
- if (!meta.entryPoint) return;
38
- results[path.basename(meta.entryPoint)] = {
39
- 'Output file': path.basename(outputPath),
40
- Size: prettyBytes(meta.bytes),
41
- 'Size (gzip)': prettyBytes(compressedSizes[outputPath].gzip),
42
- 'Size (brotli)': prettyBytes(compressedSizes[outputPath].brotli),
48
+ Object.entries(manifest).forEach(([entryPoint, assetPath]) => {
49
+ const sizes = compressedSizes[assetPath];
50
+ results[entryPoint] = {
51
+ 'Output file': assetPath,
52
+ Size: prettyBytes(sizes.raw),
53
+ 'Size (gzip)': prettyBytes(sizes.gzip),
54
+ 'Size (brotli)': prettyBytes(sizes.brotli),
43
55
  };
44
56
  });
45
57
  console.table(results);
package/src/index.test.ts CHANGED
@@ -6,26 +6,45 @@ import { assert } from 'chai';
6
6
  import express from 'express';
7
7
  import fetch from 'node-fetch';
8
8
 
9
- import { init, compiledScriptPath, handler, CompiledAssetsOptions, build } from './index';
9
+ import {
10
+ init,
11
+ close,
12
+ handler,
13
+ build,
14
+ compiledScriptPath,
15
+ compiledStylesheetPath,
16
+ type CompiledAssetsOptions,
17
+ } from './index';
10
18
 
11
19
  async function testProject(options: CompiledAssetsOptions) {
12
20
  await tmp.withDir(
13
21
  async (dir) => {
14
- const scriptsRoot = path.join(dir.path, 'assets', 'scripts');
22
+ // macOS does weird things with symlinks in its tmp directories. Resolve
23
+ // the real path so that our asset-building machinery doesn't get confused.
24
+ const tmpDir = await fs.realpath(dir.path);
25
+
26
+ const scriptsRoot = path.join(tmpDir, 'assets', 'scripts');
15
27
  await fs.ensureDir(scriptsRoot);
16
28
 
29
+ const stylesRoot = path.join(tmpDir, 'assets', 'stylesheets');
30
+ await fs.ensureDir(stylesRoot);
31
+
17
32
  const jsScriptPath = path.join(scriptsRoot, 'foo.js');
33
+ await fs.writeFile(jsScriptPath, 'console.log("foo")');
34
+
18
35
  const tsScriptPath = path.join(scriptsRoot, 'bar.ts');
19
- fs.writeFile(jsScriptPath, 'console.log("foo")');
20
- fs.writeFile(tsScriptPath, 'interface Foo {};\n\nconsole.log("bar")');
36
+ await fs.writeFile(tsScriptPath, 'interface Foo {};\n\nconsole.log("bar")');
37
+
38
+ const stylesPath = path.join(stylesRoot, 'baz.css');
39
+ await fs.writeFile(stylesPath, 'body { color: red; }');
21
40
 
22
41
  if (!options.dev) {
23
- await build(path.join(dir.path, 'assets'), path.join(dir.path, 'public', 'build'));
42
+ await build(path.join(tmpDir, 'assets'), path.join(tmpDir, 'public', 'build'));
24
43
  }
25
44
 
26
- init({
27
- sourceDirectory: path.join(dir.path, 'assets'),
28
- buildDirectory: path.join(dir.path, 'public', 'build'),
45
+ await init({
46
+ sourceDirectory: path.join(tmpDir, 'assets'),
47
+ buildDirectory: path.join(tmpDir, 'public', 'build'),
29
48
  publicPath: '/build',
30
49
  ...options,
31
50
  });
@@ -36,9 +55,15 @@ async function testProject(options: CompiledAssetsOptions) {
36
55
  const server = app.listen(port);
37
56
 
38
57
  try {
39
- const res = await fetch(`http://localhost:${port}${compiledScriptPath('foo.js')}`);
40
- assert.isTrue(res.ok);
41
- assert.match(await res.text(), /console\.log\("foo"\)/);
58
+ const jsRes = await fetch(`http://localhost:${port}${compiledScriptPath('foo.js')}`);
59
+ assert.isTrue(jsRes.ok);
60
+ assert.match(await jsRes.text(), /console\.log\("foo"\)/);
61
+
62
+ const cssRes = await fetch(`http://localhost:${port}${compiledStylesheetPath('baz.css')}`);
63
+ assert.isTrue(cssRes.ok);
64
+ const cssText = await cssRes.text();
65
+ assert.match(cssText, /body\s*\{/);
66
+ assert.match(cssText, /color:\s*red/);
42
67
  } finally {
43
68
  server.close();
44
69
  }
@@ -50,6 +75,8 @@ async function testProject(options: CompiledAssetsOptions) {
50
75
  }
51
76
 
52
77
  describe('compiled-assets', () => {
78
+ afterEach(async () => close());
79
+
53
80
  it('works in dev mode', async () => {
54
81
  await testProject({ dev: true });
55
82
  });
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { RequestHandler } from 'express';
2
2
  import expressStaticGzip from 'express-static-gzip';
3
- import esbuild from 'esbuild';
3
+ import esbuild, { Metafile } from 'esbuild';
4
4
  import path from 'path';
5
5
  import globby from 'globby';
6
6
  import fs from 'fs-extra';
7
+ import http from 'node:http';
7
8
  import { html, HtmlSafeString } from '@prairielearn/html';
8
9
 
9
10
  const DEFAULT_OPTIONS = {
@@ -13,9 +14,11 @@ const DEFAULT_OPTIONS = {
13
14
  publicPath: '/build/',
14
15
  };
15
16
 
17
+ type AssetsManifest = Record<string, string>;
18
+
16
19
  export interface CompiledAssetsOptions {
17
20
  /**
18
- * Whether the app is running in dev mode. If dev modde is enabled, then
21
+ * Whether the app is running in dev mode. If dev mode is enabled, then
19
22
  * assets will be built on the fly as they're requested. Otherwise, assets
20
23
  * should have been pre-compiled to the `buildDirectory` directory.
21
24
  */
@@ -29,15 +32,51 @@ export interface CompiledAssetsOptions {
29
32
  }
30
33
 
31
34
  let options: Required<CompiledAssetsOptions> = { ...DEFAULT_OPTIONS };
35
+ let esbuildContext: esbuild.BuildContext | null = null;
36
+ let esbuildServer: esbuild.ServeResult | null = null;
32
37
 
33
- export function init(newOptions: Partial<CompiledAssetsOptions>): void {
38
+ export async function init(newOptions: Partial<CompiledAssetsOptions>): Promise<void> {
34
39
  options = {
35
40
  ...DEFAULT_OPTIONS,
36
41
  ...newOptions,
37
42
  };
43
+
38
44
  if (!options.publicPath.endsWith('/')) {
39
45
  options.publicPath += '/';
40
46
  }
47
+
48
+ if (options.dev) {
49
+ // Use esbuild's asset server in development.
50
+ //
51
+ // Note that esbuild doesn't support globs, so the server will not pick up
52
+ // new entrypoints that are added while the server is running.
53
+ const sourceGlob = path.join(options.sourceDirectory, '*', '*.{js,ts,css}');
54
+ const sourcePaths = await globby(sourceGlob);
55
+ esbuildContext = await esbuild.context({
56
+ entryPoints: sourcePaths,
57
+ target: 'es6',
58
+ format: 'iife',
59
+ sourcemap: 'inline',
60
+ bundle: true,
61
+ write: false,
62
+ loader: {
63
+ '.woff': 'file',
64
+ '.woff2': 'file',
65
+ },
66
+ outbase: options.sourceDirectory,
67
+ outdir: options.buildDirectory,
68
+ entryNames: '[dir]/[name]',
69
+ });
70
+
71
+ esbuildServer = await esbuildContext.serve();
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Shuts down the development assets compiler if it is running.
77
+ */
78
+ export async function close() {
79
+ esbuildContext?.dispose();
41
80
  }
42
81
 
43
82
  export function assertConfigured(): void {
@@ -64,94 +103,81 @@ export function handler(): RequestHandler {
64
103
  });
65
104
  }
66
105
 
106
+ if (!esbuildServer) {
107
+ throw new Error('esbuild server not initialized');
108
+ }
109
+
110
+ const { host, port } = esbuildServer;
111
+
67
112
  // We're running in dev mode, so we need to boot up ESBuild to start building
68
113
  // and watching our assets.
69
114
  return function (req, res) {
70
- // Strip leading slash from `req.url`.
71
- let assetPath = req.url;
72
- if (assetPath.startsWith('/')) {
73
- assetPath = assetPath.slice(1);
74
- }
75
-
76
- const resolvedSourceDirectory = path.resolve(options?.sourceDirectory);
77
- const resolvedAssetPath = path.resolve(resolvedSourceDirectory, assetPath);
78
-
79
- if (!resolvedAssetPath.startsWith(resolvedSourceDirectory)) {
80
- // Probably path traversal.
81
- res.status(404).send('Not found');
82
- return;
83
- }
84
-
85
- // esbuild should be fast enough that we can just build everything on the
86
- // fly as it's requested! This is probably just for prototyping though. We
87
- // should use some kind of caching to ensure that local dev stays fast.
88
- esbuild
89
- .build({
90
- entryPoints: [resolvedAssetPath],
91
- target: 'es6',
92
- format: 'iife',
93
- sourcemap: 'inline',
94
- bundle: true,
95
- write: false,
96
- })
97
- .then(
98
- (buildResult) => {
99
- res
100
- .setHeader('Content-Type', 'application/javascript; charset=UTF-8')
101
- .status(200)
102
- .send(buildResult.outputFiles[0].text);
103
- },
104
- (buildError: Error) => {
105
- res.status(500).send(buildError.message);
106
- }
107
- );
115
+ const proxyReq = http.request(
116
+ {
117
+ hostname: host,
118
+ port,
119
+ path: req.url,
120
+ method: req.method,
121
+ headers: req.headers,
122
+ },
123
+ (proxyRes) => {
124
+ res.writeHead(proxyRes.statusCode ?? 500, proxyRes.headers);
125
+ proxyRes.pipe(res, { end: true });
126
+ }
127
+ );
128
+ req.pipe(proxyReq, { end: true });
108
129
  };
109
130
  }
110
131
 
111
- let cachedScriptsManifest: Record<string, string> | null = null;
112
- function readScriptsManifest(): Record<string, string> {
132
+ let cachedManifest: AssetsManifest | null = null;
133
+ function readManifest(): AssetsManifest {
113
134
  assertConfigured();
114
135
 
115
- if (!cachedScriptsManifest) {
116
- const manifestPath = path.join(options.buildDirectory, 'scripts', 'manifest.json');
117
- cachedScriptsManifest = fs.readJSONSync(manifestPath) as Record<string, string>;
136
+ if (!cachedManifest) {
137
+ const manifestPath = path.join(options.buildDirectory, 'manifest.json');
138
+ cachedManifest = fs.readJSONSync(manifestPath) as AssetsManifest;
118
139
  }
119
140
 
120
- return cachedScriptsManifest;
141
+ return cachedManifest;
121
142
  }
122
143
 
123
- export function compiledScriptPath(sourceFile: string): string {
144
+ function compiledPath(type: 'scripts' | 'stylesheets', sourceFile: string): string {
124
145
  assertConfigured();
146
+ const sourceFilePath = `${type}/${sourceFile}`;
125
147
 
126
148
  if (options.dev) {
127
- return options.publicPath + 'scripts/' + sourceFile;
149
+ return options.publicPath + sourceFilePath.replace(/\.(js|ts)x?$/, '.js');
128
150
  }
129
151
 
130
- const scriptsManifest = readScriptsManifest();
131
- const scriptPath = scriptsManifest[sourceFile];
132
- if (!scriptPath) {
133
- throw new Error(`Unknown script: ${sourceFile}`);
152
+ const manifest = readManifest();
153
+ const assetPath = manifest[sourceFilePath];
154
+ if (!assetPath) {
155
+ throw new Error(`Unknown ${type} asset: ${sourceFile}`);
134
156
  }
135
157
 
136
- return options.publicPath + 'scripts/' + scriptPath;
158
+ return `${options.publicPath}/${assetPath}`;
159
+ }
160
+
161
+ export function compiledScriptPath(sourceFile: string): string {
162
+ return compiledPath('scripts', sourceFile);
163
+ }
164
+
165
+ export function compiledStylesheetPath(sourceFile: string): string {
166
+ return compiledPath('stylesheets', sourceFile);
137
167
  }
138
168
 
139
169
  export function compiledScriptTag(sourceFile: string): HtmlSafeString {
140
170
  return html`<script src="${compiledScriptPath(sourceFile)}"></script>`;
141
171
  }
142
172
 
143
- export async function build(
144
- sourceDirectory: string,
145
- buildDirectory: string
146
- ): Promise<esbuild.Metafile> {
147
- // Remove existing assets to ensure that no stale assets are left behind.
148
- await fs.remove(buildDirectory);
173
+ export function compiledStylesheetTag(sourceFile: string): HtmlSafeString {
174
+ return html`<link rel="stylesheet" href="${compiledStylesheetPath(sourceFile)}" />`;
175
+ }
149
176
 
150
- const scriptsSourceRoot = path.resolve(sourceDirectory, 'scripts');
151
- const scriptsBuildRoot = path.resolve(buildDirectory, 'scripts');
152
- await fs.ensureDir(scriptsBuildRoot);
177
+ async function buildAssets(sourceDirectory: string, buildDirectory: string) {
178
+ await fs.ensureDir(buildDirectory);
153
179
 
154
- const files = await globby(path.join(scriptsSourceRoot, '*.{js,jsx,ts,tsx}'));
180
+ const files = await globby(path.join(sourceDirectory, '*/*.{js,jsx,ts,tsx,css}'));
155
181
  const buildResult = await esbuild.build({
156
182
  entryPoints: files,
157
183
  target: 'es6',
@@ -159,25 +185,46 @@ export async function build(
159
185
  sourcemap: 'linked',
160
186
  bundle: true,
161
187
  minify: true,
162
- entryNames: '[name]-[hash]',
163
- outdir: scriptsBuildRoot,
188
+ loader: {
189
+ '.woff': 'file',
190
+ '.woff2': 'file',
191
+ },
192
+ entryNames: '[dir]/[name]-[hash]',
193
+ outbase: sourceDirectory,
194
+ outdir: buildDirectory,
164
195
  metafile: true,
165
196
  });
166
197
 
167
- // Write asset manifest so that we can map from "input" names to built names
168
- // at runtime.
169
- const { metafile } = buildResult;
198
+ return buildResult.metafile;
199
+ }
200
+
201
+ function makeManifest(
202
+ metafile: Metafile,
203
+ sourceDirectory: string,
204
+ buildDirectory: string
205
+ ): Record<string, string> {
170
206
  const manifest: Record<string, string> = {};
171
207
  Object.entries(metafile.outputs).forEach(([outputPath, meta]) => {
172
208
  if (!meta.entryPoint) return;
173
209
 
174
- const entryPath = path.basename(meta.entryPoint);
175
- const assetPath = path.basename(outputPath);
176
-
210
+ const entryPath = path.relative(sourceDirectory, meta.entryPoint);
211
+ const assetPath = path.relative(buildDirectory, outputPath);
177
212
  manifest[entryPath] = assetPath;
178
213
  });
179
- const manifestPath = path.join(scriptsBuildRoot, 'manifest.json');
214
+ return manifest;
215
+ }
216
+
217
+ export async function build(
218
+ sourceDirectory: string,
219
+ buildDirectory: string
220
+ ): Promise<AssetsManifest> {
221
+ // Remove existing assets to ensure that no stale assets are left behind.
222
+ await fs.remove(buildDirectory);
223
+
224
+ const metafile = await buildAssets(sourceDirectory, buildDirectory);
225
+ const manifest = makeManifest(metafile, sourceDirectory, buildDirectory);
226
+ const manifestPath = path.join(buildDirectory, 'manifest.json');
180
227
  await fs.writeJSON(manifestPath, manifest);
181
228
 
182
- return metafile;
229
+ return manifest;
183
230
  }
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "@prairielearn/tsconfig",
2
+ "extends": "@prairielearn/tsconfig/tsconfig.package.json",
3
3
  "compilerOptions": {
4
4
  "outDir": "./dist",
5
5
  "rootDir": "./src",