-
-
Notifications
You must be signed in to change notification settings - Fork 75
/
bundler.ts
156 lines (131 loc) Β· 4.63 KB
/
bundler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { getDenoConfig, getImportMap, merge } from "../core/utils.ts";
import { toFileUrl } from "../deps/path.ts";
import { createGraph, load, LoadResponse } from "../deps/graph.ts";
import { Page } from "../core/filesystem.ts";
import type { Site } from "../core.ts";
import type { LoadResponseModule } from "../deps/graph.ts";
export interface Options {
/** The list of extensions this plugin applies to */
extensions: string[];
/** Set `true` to generate source map files */
sourceMap: boolean;
/** The options for Deno.emit */
options: Deno.EmitOptions;
}
// Default options
export const defaults: Options = {
extensions: [".ts", ".js"],
sourceMap: false,
options: {},
};
const denoConfig = await getDenoConfig();
/** A plugin to load all .js, .ts, .jsx, .tsx files and bundle them using Deno.emit() */
export default function (userOptions?: Partial<Options>) {
const options = merge(defaults, userOptions);
return (site: Site) => {
const sources: Record<string, string> = {};
let importMap: Deno.ImportMap | undefined;
let importMapPath: string | undefined;
// Load import map
site.addEventListener("beforeBuild", async () => {
importMapPath = options.options.importMapPath || denoConfig?.importMap;
importMap = options.options.importMap ||
(importMapPath ? await getImportMap(importMapPath) : undefined);
});
// Refresh updated files
site.addEventListener("beforeUpdate", (event) => {
event.files?.forEach((file) => {
const specifier = toFileUrl(site.src(file)).href;
delete sources[specifier];
});
});
site.loadAssets(options.extensions);
/**
* For bundle, we need to load all the files sources
* before emitting the bundle
*/
if (options.options.bundle) {
// Load all source files and save the content in `sources`
site.process(options.extensions, (file: Page) => {
const specifier = getSpecifier(file);
sources[specifier] = file.content as string;
});
// Load all other dependencies and save the content in `sources`
site.process(options.extensions, async (file: Page) => {
const specifier = getSpecifier(file);
await createGraph(specifier, {
resolve(specifier, referrer) {
return isBare(specifier)
? getFileSpecifier(specifier)
: new URL(specifier, referrer).href;
},
async load(
specifier: string,
isDynamic: boolean,
): Promise<LoadResponse | undefined> {
if (isDynamic) {
return;
}
if (specifier in sources) {
return {
kind: "module",
specifier: specifier,
content: sources[specifier],
};
}
const response = await load(specifier) as LoadResponseModule;
if (response) {
sources[specifier] = response.content;
return response;
}
},
});
});
}
// Now we are ready to emit the entries
site.process(options.extensions, async (file: Page) => {
const specifier = getSpecifier(file);
const { files } = await Deno.emit(specifier, {
...options.options,
sources: {
...sources,
[specifier]: file.content as string,
},
importMap,
importMapPath,
});
const content = files[specifier] || files[specifier + ".js"] ||
files["deno:///bundle.js"];
if (content) {
file.content = fixExtensions(content);
file.updateDest({ ext: ".js" });
}
const mapContent = files[specifier + ".map"] ||
files[specifier + ".js.map"] || files["deno:///bundle.js.map"];
if (options.sourceMap && mapContent) {
const mapFile = Page.create(file.dest.path + ".js.map", mapContent);
site.pages.push(mapFile);
}
});
function getSpecifier(file: Page) {
file._data.specifier ||=
toFileUrl(site.src(file.data.url as string)).href;
return file._data.specifier as string;
}
function getFileSpecifier(file: string) {
for (const key in importMap?.imports) {
if (file.startsWith(key)) {
return importMap?.imports[key] + file.slice(key.length);
}
}
throw new Error(`Invalid specifier ${file}`);
}
};
}
/** Replace all .ts, .tsx and .jsx files with .js files */
function fixExtensions(content: string) {
return content.replaceAll(/\.(ts|tsx|jsx)("|')/ig, ".js$2");
}
function isBare(specifier: string) {
return !specifier.startsWith(".") && !specifier.includes("://");
}