|
1 |
| -import { createChannel } from "../channel"; |
2 |
| -import { type Manifest } from "../manifest"; |
3 |
| -import * as AssetsCompiler from "./assets"; |
| 1 | +import * as path from "path"; |
| 2 | + |
4 | 3 | import type { Context } from "./context";
|
5 |
| -import * as ServerCompiler from "./server"; |
| 4 | +import * as CSS from "./css"; |
| 5 | +import * as JS from "./js"; |
| 6 | +import * as Server from "./server"; |
| 7 | +import * as Channel from "../channel"; |
| 8 | +import type { Manifest } from "../manifest"; |
| 9 | +import { create as createManifest, write as writeManifest } from "./manifest"; |
6 | 10 |
|
7 | 11 | type Compiler = {
|
8 |
| - compile: () => Promise<Manifest | undefined>; |
9 |
| - dispose: () => void; |
| 12 | + compile: () => Promise<Manifest>; |
| 13 | + cancel: () => Promise<void>; |
| 14 | + dispose: () => Promise<void>; |
10 | 15 | };
|
11 | 16 |
|
12 | 17 | export let create = async (ctx: Context): Promise<Compiler> => {
|
| 18 | + // channels _should_ be scoped to a build, not a compiler |
| 19 | + // but esbuild doesn't have an API for passing build-specific arguments for rebuilds |
| 20 | + // so instead use a mutable reference (`channels`) that is compiler-scoped |
| 21 | + // and gets reset on each build |
13 | 22 | let channels = {
|
14 |
| - manifest: createChannel<Manifest>(), |
| 23 | + cssBundleHref: undefined as unknown as Channel.Type<string | undefined>, |
| 24 | + manifest: undefined as unknown as Channel.Type<Manifest>, |
| 25 | + }; |
| 26 | + |
| 27 | + let subcompiler = { |
| 28 | + css: await CSS.createCompiler(ctx), |
| 29 | + js: await JS.createCompiler(ctx, channels), |
| 30 | + server: await Server.createCompiler(ctx, channels), |
15 | 31 | };
|
16 | 32 |
|
17 |
| - let assets = await AssetsCompiler.create(ctx, channels); |
18 |
| - let server = await ServerCompiler.create(ctx, channels); |
| 33 | + let compile = async () => { |
| 34 | + let hasThrown = false; |
| 35 | + let cancelAndThrow = async (error: unknown) => { |
| 36 | + // An earlier error from a failed task has already been thrown; ignore this error. |
| 37 | + // Safe to cast as `never` here as subsequent errors are only thrown from canceled tasks. |
| 38 | + if (hasThrown) return undefined as never; |
| 39 | + |
| 40 | + // resolve channels with error so that downstream tasks don't hang waiting for results from upstream tasks |
| 41 | + channels.cssBundleHref.err(); |
| 42 | + channels.manifest.err(); |
| 43 | + |
| 44 | + // optimization: cancel tasks |
| 45 | + subcompiler.css.cancel(); |
| 46 | + subcompiler.js.cancel(); |
| 47 | + subcompiler.server.cancel(); |
| 48 | + |
| 49 | + // Only throw the first error encountered during compilation |
| 50 | + // otherwise subsequent errors will be unhandled and will crash the compiler. |
| 51 | + // `try`/`catch` won't handle subsequent errors either, so that isn't a viable alternative. |
| 52 | + // `Promise.all` _could_ be used, but the resulting promise chaining is complex and hard to follow. |
| 53 | + hasThrown = true; |
| 54 | + throw error; |
| 55 | + }; |
| 56 | + |
| 57 | + // reset channels |
| 58 | + channels.cssBundleHref = Channel.create(); |
| 59 | + channels.manifest = Channel.create(); |
| 60 | + |
| 61 | + // kickoff compilations in parallel |
| 62 | + let tasks = { |
| 63 | + css: subcompiler.css.compile().catch(cancelAndThrow), |
| 64 | + js: subcompiler.js.compile().catch(cancelAndThrow), |
| 65 | + server: subcompiler.server.compile().catch(cancelAndThrow), |
| 66 | + }; |
| 67 | + |
| 68 | + // keep track of manually written artifacts |
| 69 | + let writes: { |
| 70 | + cssBundle?: Promise<void>; |
| 71 | + manifest?: Promise<void>; |
| 72 | + server?: Promise<void>; |
| 73 | + } = {}; |
| 74 | + |
| 75 | + // css compilation |
| 76 | + let css = await tasks.css; |
| 77 | + |
| 78 | + // css bundle |
| 79 | + let cssBundleHref = |
| 80 | + css.bundle && |
| 81 | + ctx.config.publicPath + |
| 82 | + path.relative( |
| 83 | + ctx.config.assetsBuildDirectory, |
| 84 | + path.resolve(css.bundle.path) |
| 85 | + ); |
| 86 | + channels.cssBundleHref.ok(cssBundleHref); |
| 87 | + if (css.bundle) { |
| 88 | + writes.cssBundle = CSS.writeBundle(ctx, css.outputFiles); |
| 89 | + } |
| 90 | + |
| 91 | + // js compilation (implicitly writes artifacts/js) |
| 92 | + // TODO: js task should not return metafile, but rather js assets |
| 93 | + let { metafile, hmr } = await tasks.js; |
| 94 | + |
| 95 | + // artifacts/manifest |
| 96 | + let manifest = await createManifest({ |
| 97 | + config: ctx.config, |
| 98 | + cssBundleHref, |
| 99 | + metafile, |
| 100 | + hmr, |
| 101 | + }); |
| 102 | + channels.manifest.ok(manifest); |
| 103 | + writes.manifest = writeManifest(ctx.config, manifest); |
| 104 | + |
| 105 | + // server compilation |
| 106 | + let serverFiles = await tasks.server; |
| 107 | + // artifacts/server |
| 108 | + writes.server = Server.write(ctx.config, serverFiles); |
| 109 | + |
| 110 | + await Promise.all(Object.values(writes)); |
| 111 | + return manifest; |
| 112 | + }; |
19 | 113 | return {
|
20 |
| - compile: async () => { |
21 |
| - channels.manifest = createChannel(); |
22 |
| - try { |
23 |
| - let [manifest] = await Promise.all([ |
24 |
| - assets.compile(), |
25 |
| - server.compile(), |
26 |
| - ]); |
27 |
| - |
28 |
| - return manifest; |
29 |
| - } catch (error: unknown) { |
30 |
| - ctx.options.onCompileFailure?.(error as Error); |
31 |
| - return undefined; |
32 |
| - } |
| 114 | + compile, |
| 115 | + cancel: async () => { |
| 116 | + await Promise.all(Object.values(subcompiler).map((sub) => sub.cancel())); |
33 | 117 | },
|
34 |
| - dispose: () => { |
35 |
| - assets.dispose(); |
36 |
| - server.dispose(); |
| 118 | + dispose: async () => { |
| 119 | + await Promise.all(Object.values(subcompiler).map((sub) => sub.dispose())); |
37 | 120 | },
|
38 | 121 | };
|
39 | 122 | };
|
0 commit comments