@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 +14 -0
- package/dist/cli.js +22 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +99 -62
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +23 -10
- package/dist/index.test.js.map +1 -1
- package/package.json +8 -8
- package/src/cli.ts +29 -17
- package/src/index.test.ts +38 -11
- package/src/index.ts +121 -74
- package/tsconfig.json +1 -1
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
|
-
|
|
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.
|
|
22
|
-
const
|
|
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(`${
|
|
26
|
-
await fs_extra_1.default.writeFile(`${
|
|
27
|
-
compressedSizes[
|
|
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(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Size: (0, pretty_bytes_1.default)(
|
|
40
|
-
'Size (
|
|
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;
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
91
|
-
function
|
|
107
|
+
let cachedManifest = null;
|
|
108
|
+
function readManifest() {
|
|
92
109
|
assertConfigured();
|
|
93
|
-
if (!
|
|
94
|
-
const manifestPath = path_1.default.join(options.buildDirectory, '
|
|
95
|
-
|
|
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
|
|
114
|
+
return cachedManifest;
|
|
98
115
|
}
|
|
99
|
-
function
|
|
116
|
+
function compiledPath(type, sourceFile) {
|
|
100
117
|
assertConfigured();
|
|
118
|
+
const sourceFilePath = `${type}/${sourceFile}`;
|
|
101
119
|
if (options.dev) {
|
|
102
|
-
return options.publicPath + '
|
|
120
|
+
return options.publicPath + sourceFilePath.replace(/\.(js|ts)x?$/, '.js');
|
|
103
121
|
}
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
107
|
-
throw new Error(`Unknown
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
await fs_extra_1.default.ensureDir(
|
|
122
|
-
const files = await (0, globby_1.default)(path_1.default.join(
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
|
142
|
-
const assetPath = path_1.default.
|
|
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
|
-
|
|
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
|
|
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":";;;;;;AACA,8EAAoD;AACpD,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.test.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
21
|
-
|
|
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(
|
|
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(
|
|
27
|
-
buildDirectory: path_1.default.join(
|
|
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
|
|
37
|
-
chai_1.assert.isTrue(
|
|
38
|
-
chai_1.assert.match(await
|
|
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
|
});
|
package/dist/index.test.js.map
CHANGED
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/compiled-assets",
|
|
3
|
-
"version": "
|
|
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.
|
|
18
|
-
"commander": "^10.0.
|
|
19
|
-
"esbuild": "^0.17.
|
|
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.
|
|
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.
|
|
33
|
+
"node-fetch": "^2.6.11",
|
|
34
34
|
"ts-node": "^10.9.1",
|
|
35
|
-
"typescript": "^
|
|
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
|
-
|
|
15
|
-
const metafile = await build(source, destination);
|
|
14
|
+
type CompressedSizes = Record<string, Record<string, number>>;
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
22
|
-
const
|
|
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(`${
|
|
26
|
-
await fs.writeFile(`${
|
|
27
|
-
compressedSizes[
|
|
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(
|
|
37
|
-
|
|
38
|
-
results[
|
|
39
|
-
'Output file':
|
|
40
|
-
Size: prettyBytes(
|
|
41
|
-
'Size (gzip)': prettyBytes(
|
|
42
|
-
'Size (brotli)': prettyBytes(
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
20
|
-
|
|
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(
|
|
42
|
+
await build(path.join(tmpDir, 'assets'), path.join(tmpDir, 'public', 'build'));
|
|
24
43
|
}
|
|
25
44
|
|
|
26
|
-
init({
|
|
27
|
-
sourceDirectory: path.join(
|
|
28
|
-
buildDirectory: path.join(
|
|
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
|
|
40
|
-
assert.isTrue(
|
|
41
|
-
assert.match(await
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
112
|
-
function
|
|
132
|
+
let cachedManifest: AssetsManifest | null = null;
|
|
133
|
+
function readManifest(): AssetsManifest {
|
|
113
134
|
assertConfigured();
|
|
114
135
|
|
|
115
|
-
if (!
|
|
116
|
-
const manifestPath = path.join(options.buildDirectory, '
|
|
117
|
-
|
|
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
|
|
141
|
+
return cachedManifest;
|
|
121
142
|
}
|
|
122
143
|
|
|
123
|
-
|
|
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 + '
|
|
149
|
+
return options.publicPath + sourceFilePath.replace(/\.(js|ts)x?$/, '.js');
|
|
128
150
|
}
|
|
129
151
|
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
133
|
-
throw new Error(`Unknown
|
|
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
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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(
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
175
|
-
const assetPath = path.
|
|
176
|
-
|
|
210
|
+
const entryPath = path.relative(sourceDirectory, meta.entryPoint);
|
|
211
|
+
const assetPath = path.relative(buildDirectory, outputPath);
|
|
177
212
|
manifest[entryPath] = assetPath;
|
|
178
213
|
});
|
|
179
|
-
|
|
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
|
|
229
|
+
return manifest;
|
|
183
230
|
}
|
package/tsconfig.json
CHANGED