vantris 0.2.0 → 0.3.0
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/dist/chunk-LRSN7SF4.js +1117 -0
- package/dist/chunk-LRSN7SF4.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/index.d.ts +152 -2
- package/dist/index.js +1 -1
- package/package.json +20 -2
- package/dist/chunk-OFLPPG2U.js +0 -600
- package/dist/chunk-OFLPPG2U.js.map +0 -1
|
@@ -0,0 +1,1117 @@
|
|
|
1
|
+
import { pathToFileURL } from 'url';
|
|
2
|
+
import { readFile, stat, rm, mkdir, cp, writeFile, realpath } from 'fs/promises';
|
|
3
|
+
import { sep, join, basename, relative, isAbsolute, resolve, dirname, extname } from 'path';
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
import { H3, toNodeHandler, getRequestURL } from 'h3';
|
|
6
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
|
+
import { transform } from 'esbuild';
|
|
8
|
+
import MagicString from 'magic-string';
|
|
9
|
+
import { rolldown } from 'rolldown';
|
|
10
|
+
import { bundleAsync } from 'lightningcss';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import { watch } from 'chokidar';
|
|
13
|
+
|
|
14
|
+
// src/shared/constants.ts
|
|
15
|
+
var APP_NAME = "vantris";
|
|
16
|
+
var VERSION = "0.3.0";
|
|
17
|
+
var HTML_ENTRY_FILENAME = "index.html";
|
|
18
|
+
var DEFAULTS = {
|
|
19
|
+
root: ".",
|
|
20
|
+
rootDir: "./src",
|
|
21
|
+
publicDir: "./public",
|
|
22
|
+
outDir: "./dist"
|
|
23
|
+
};
|
|
24
|
+
var DEV_DEFAULTS = {
|
|
25
|
+
port: 3e3,
|
|
26
|
+
host: "localhost"
|
|
27
|
+
};
|
|
28
|
+
var BUILD_DEFAULTS = {
|
|
29
|
+
minify: true,
|
|
30
|
+
sourcemap: false,
|
|
31
|
+
assetsDir: "assets"
|
|
32
|
+
};
|
|
33
|
+
var ASSET_EXTENSIONS = [
|
|
34
|
+
// images
|
|
35
|
+
".svg",
|
|
36
|
+
".png",
|
|
37
|
+
".jpg",
|
|
38
|
+
".jpeg",
|
|
39
|
+
".gif",
|
|
40
|
+
".webp",
|
|
41
|
+
".avif",
|
|
42
|
+
".ico",
|
|
43
|
+
".bmp",
|
|
44
|
+
// fonts
|
|
45
|
+
".woff",
|
|
46
|
+
".woff2",
|
|
47
|
+
".ttf",
|
|
48
|
+
".otf",
|
|
49
|
+
".eot",
|
|
50
|
+
// media
|
|
51
|
+
".mp4",
|
|
52
|
+
".webm",
|
|
53
|
+
".ogg",
|
|
54
|
+
".mp3",
|
|
55
|
+
".wav",
|
|
56
|
+
".flac",
|
|
57
|
+
".aac"
|
|
58
|
+
];
|
|
59
|
+
var RELOAD_MESSAGE = "reload";
|
|
60
|
+
var CONFIG_FILENAMES = [
|
|
61
|
+
"vantris.config.ts",
|
|
62
|
+
"vantris.config.js",
|
|
63
|
+
"vantris.config.mjs"
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// src/shared/errors.ts
|
|
67
|
+
var VantrisError = class extends Error {
|
|
68
|
+
name = "VantrisError";
|
|
69
|
+
constructor(message, options) {
|
|
70
|
+
super(message, options);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var ConfigError = class extends VantrisError {
|
|
74
|
+
name = "ConfigError";
|
|
75
|
+
};
|
|
76
|
+
var HtmlEntryError = class extends VantrisError {
|
|
77
|
+
name = "HtmlEntryError";
|
|
78
|
+
};
|
|
79
|
+
var BuildError = class extends VantrisError {
|
|
80
|
+
name = "BuildError";
|
|
81
|
+
};
|
|
82
|
+
var NotImplementedError = class extends VantrisError {
|
|
83
|
+
name = "NotImplementedError";
|
|
84
|
+
};
|
|
85
|
+
function isVantrisError(error) {
|
|
86
|
+
return error instanceof VantrisError;
|
|
87
|
+
}
|
|
88
|
+
async function isFile(path) {
|
|
89
|
+
try {
|
|
90
|
+
return (await stat(path)).isFile();
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function isDirectory(path) {
|
|
96
|
+
try {
|
|
97
|
+
return (await stat(path)).isDirectory();
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function readTextFile(path) {
|
|
103
|
+
return readFile(path, "utf8");
|
|
104
|
+
}
|
|
105
|
+
async function ensureDir(path) {
|
|
106
|
+
await mkdir(path, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
function resolveFrom(base, target) {
|
|
109
|
+
return isAbsolute(target) ? target : resolve(base, target);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/config/load.ts
|
|
113
|
+
async function loadConfig(options) {
|
|
114
|
+
const { cwd, logger } = options;
|
|
115
|
+
const file = options.configFile ? resolveFrom(cwd, options.configFile) : await findConfigFile(cwd);
|
|
116
|
+
if (!file) {
|
|
117
|
+
logger.debug("No config file found; using defaults.");
|
|
118
|
+
return { config: {}, file: null };
|
|
119
|
+
}
|
|
120
|
+
logger.debug(`Loading config from ${file}`);
|
|
121
|
+
const config = await importConfig(file);
|
|
122
|
+
return { config, file };
|
|
123
|
+
}
|
|
124
|
+
async function findConfigFile(cwd) {
|
|
125
|
+
for (const name of CONFIG_FILENAMES) {
|
|
126
|
+
const candidate = resolveFrom(cwd, name);
|
|
127
|
+
if (await isFile(candidate)) return candidate;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
async function importConfig(file) {
|
|
132
|
+
let mod;
|
|
133
|
+
try {
|
|
134
|
+
const url = `${pathToFileURL(file).href}?t=${Date.now()}`;
|
|
135
|
+
mod = await import(url);
|
|
136
|
+
} catch (cause) {
|
|
137
|
+
throw new ConfigError(`Failed to load config file: ${file}`, { cause });
|
|
138
|
+
}
|
|
139
|
+
const exported = mod.default;
|
|
140
|
+
if (exported === void 0) {
|
|
141
|
+
throw new ConfigError(
|
|
142
|
+
`Config file "${file}" has no default export. Export your config with \`export default defineConfig({ ... })\`.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return normalise(exported, file);
|
|
146
|
+
}
|
|
147
|
+
async function normalise(input, file) {
|
|
148
|
+
const value = typeof input === "function" ? await input() : input;
|
|
149
|
+
if (value === null || typeof value !== "object") {
|
|
150
|
+
throw new ConfigError(
|
|
151
|
+
`Config file "${file}" must export an object (or a function returning one).`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/config/resolve.ts
|
|
158
|
+
function resolveConfig(raw, cwd, configFile = null) {
|
|
159
|
+
const root = resolveFrom(cwd, raw.root ?? DEFAULTS.root);
|
|
160
|
+
const paths = {
|
|
161
|
+
root,
|
|
162
|
+
rootDir: resolveFrom(root, raw.rootDir ?? DEFAULTS.rootDir),
|
|
163
|
+
publicDir: resolveFrom(root, raw.publicDir ?? DEFAULTS.publicDir),
|
|
164
|
+
outDir: resolveFrom(root, raw.outDir ?? DEFAULTS.outDir)
|
|
165
|
+
};
|
|
166
|
+
const base = normalizeBase(raw.base ?? "/");
|
|
167
|
+
const dev2 = {
|
|
168
|
+
port: raw.dev?.port ?? DEV_DEFAULTS.port,
|
|
169
|
+
host: raw.dev?.host ?? DEV_DEFAULTS.host
|
|
170
|
+
};
|
|
171
|
+
const assetsDir = raw.build?.assetsDir ?? BUILD_DEFAULTS.assetsDir;
|
|
172
|
+
const build2 = {
|
|
173
|
+
minify: raw.build?.minify ?? BUILD_DEFAULTS.minify,
|
|
174
|
+
sourcemap: raw.build?.sourcemap ?? BUILD_DEFAULTS.sourcemap,
|
|
175
|
+
assetsDir,
|
|
176
|
+
// File-name patterns default off `assetsDir`; explicit values win.
|
|
177
|
+
entryFileNames: raw.build?.entryFileNames ?? `${assetsDir}/[name]-[hash].js`,
|
|
178
|
+
chunkFileNames: raw.build?.chunkFileNames ?? `${assetsDir}/[name]-[hash].js`,
|
|
179
|
+
assetFileNames: raw.build?.assetFileNames ?? `${assetsDir}/[name]-[hash][extname]`
|
|
180
|
+
};
|
|
181
|
+
return { raw, paths, base, dev: dev2, build: build2, configFile };
|
|
182
|
+
}
|
|
183
|
+
function normalizeBase(base) {
|
|
184
|
+
let value = base.trim() || "/";
|
|
185
|
+
if (!/^https?:\/\//.test(value) && !value.startsWith("/")) {
|
|
186
|
+
value = `/${value}`;
|
|
187
|
+
}
|
|
188
|
+
if (!value.endsWith("/")) value = `${value}/`;
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/html/parse.ts
|
|
193
|
+
var SCRIPT_TAG = /<script\b[^>]*>/gi;
|
|
194
|
+
var TYPE_MODULE = /\btype\s*=\s*["']module["']/i;
|
|
195
|
+
var SRC_ATTR = /\bsrc\s*=\s*["']([^"']+)["']/i;
|
|
196
|
+
function parseHtml(file, html) {
|
|
197
|
+
const scripts = [];
|
|
198
|
+
for (const [tag] of html.matchAll(SCRIPT_TAG)) {
|
|
199
|
+
if (!TYPE_MODULE.test(tag)) continue;
|
|
200
|
+
const src = SRC_ATTR.exec(tag)?.[1];
|
|
201
|
+
if (src) scripts.push({ src });
|
|
202
|
+
}
|
|
203
|
+
return { file, html, scripts };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/html/detect.ts
|
|
207
|
+
async function detectHtmlEntry(root) {
|
|
208
|
+
const file = resolveFrom(root, HTML_ENTRY_FILENAME);
|
|
209
|
+
if (!await isFile(file)) return null;
|
|
210
|
+
const html = await readTextFile(file);
|
|
211
|
+
return parseHtml(file, html);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/html/client.ts
|
|
215
|
+
var DEV_CLIENT_SCRIPT = `<script type="module">
|
|
216
|
+
// Injected by Vantris dev server \u2014 live reload (full page).
|
|
217
|
+
const connect = () => {
|
|
218
|
+
const ws = new WebSocket("ws://" + location.host);
|
|
219
|
+
ws.addEventListener("message", () => location.reload());
|
|
220
|
+
// Reconnect if the dev server restarts.
|
|
221
|
+
ws.addEventListener("close", () => setTimeout(connect, 1000));
|
|
222
|
+
};
|
|
223
|
+
connect();
|
|
224
|
+
</script>`;
|
|
225
|
+
function injectDevClient(html) {
|
|
226
|
+
if (html.includes("</head>")) {
|
|
227
|
+
return html.replace("</head>", `${DEV_CLIENT_SCRIPT}
|
|
228
|
+
</head>`);
|
|
229
|
+
}
|
|
230
|
+
if (html.includes("</body>")) {
|
|
231
|
+
return html.replace("</body>", `${DEV_CLIENT_SCRIPT}
|
|
232
|
+
</body>`);
|
|
233
|
+
}
|
|
234
|
+
return `${html}
|
|
235
|
+
${DEV_CLIENT_SCRIPT}`;
|
|
236
|
+
}
|
|
237
|
+
function createReloadSocket(options) {
|
|
238
|
+
const { server, logger } = options;
|
|
239
|
+
const wss = new WebSocketServer({ server });
|
|
240
|
+
const clients = /* @__PURE__ */ new Set();
|
|
241
|
+
wss.on("connection", (socket) => {
|
|
242
|
+
clients.add(socket);
|
|
243
|
+
socket.on("close", () => clients.delete(socket));
|
|
244
|
+
socket.on("error", () => clients.delete(socket));
|
|
245
|
+
});
|
|
246
|
+
wss.on("error", (error) => {
|
|
247
|
+
logger.error(`websocket error: ${error.message}`);
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
broadcastReload() {
|
|
251
|
+
for (const socket of clients) {
|
|
252
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
253
|
+
socket.send(RELOAD_MESSAGE);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
get clientCount() {
|
|
258
|
+
return clients.size;
|
|
259
|
+
},
|
|
260
|
+
close() {
|
|
261
|
+
return new Promise((resolveClose) => {
|
|
262
|
+
for (const socket of clients) socket.terminate();
|
|
263
|
+
clients.clear();
|
|
264
|
+
wss.close(() => resolveClose());
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
var MIME_TYPES = {
|
|
270
|
+
".html": "text/html; charset=utf-8",
|
|
271
|
+
".js": "text/javascript; charset=utf-8",
|
|
272
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
273
|
+
".css": "text/css; charset=utf-8",
|
|
274
|
+
".json": "application/json; charset=utf-8",
|
|
275
|
+
".svg": "image/svg+xml",
|
|
276
|
+
".png": "image/png",
|
|
277
|
+
".jpg": "image/jpeg",
|
|
278
|
+
".jpeg": "image/jpeg",
|
|
279
|
+
".gif": "image/gif",
|
|
280
|
+
".webp": "image/webp",
|
|
281
|
+
".ico": "image/x-icon",
|
|
282
|
+
".woff": "font/woff",
|
|
283
|
+
".woff2": "font/woff2",
|
|
284
|
+
".txt": "text/plain; charset=utf-8",
|
|
285
|
+
".map": "application/json; charset=utf-8"
|
|
286
|
+
};
|
|
287
|
+
var JAVASCRIPT = "text/javascript; charset=utf-8";
|
|
288
|
+
function contentTypeFor(file, transpiled = false) {
|
|
289
|
+
if (transpiled) return JAVASCRIPT;
|
|
290
|
+
return MIME_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
291
|
+
}
|
|
292
|
+
var TRANSPILE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".jsx"]);
|
|
293
|
+
function shouldTranspile(file) {
|
|
294
|
+
return TRANSPILE_EXTENSIONS.has(extname(file).toLowerCase());
|
|
295
|
+
}
|
|
296
|
+
function loaderFor(file) {
|
|
297
|
+
switch (extname(file).toLowerCase()) {
|
|
298
|
+
case ".tsx":
|
|
299
|
+
return "tsx";
|
|
300
|
+
case ".jsx":
|
|
301
|
+
return "jsx";
|
|
302
|
+
default:
|
|
303
|
+
return "ts";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function transpile(code, file) {
|
|
307
|
+
const result = await transform(code, {
|
|
308
|
+
loader: loaderFor(file),
|
|
309
|
+
format: "esm",
|
|
310
|
+
target: "es2022",
|
|
311
|
+
sourcemap: "inline",
|
|
312
|
+
sourcefile: file
|
|
313
|
+
});
|
|
314
|
+
return result.code;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/server/static.ts
|
|
318
|
+
var RESOLVE_EXTENSIONS = [".ts", ".tsx", ".mts", ".js", ".mjs", ".jsx"];
|
|
319
|
+
function createStaticLoader(options) {
|
|
320
|
+
const root = resolve(options.root);
|
|
321
|
+
const rootDir = resolve(options.rootDir);
|
|
322
|
+
const publicDir = resolve(options.publicDir);
|
|
323
|
+
return async function loadAsset(pathname) {
|
|
324
|
+
const relative4 = decodeURIComponent(pathname).replace(/^\/+/, "");
|
|
325
|
+
if (!relative4) return null;
|
|
326
|
+
const source = await resolveConfined(root, rootDir, relative4);
|
|
327
|
+
if (source) return readAsset(source);
|
|
328
|
+
const asset = await resolveConfined(publicDir, publicDir, relative4);
|
|
329
|
+
if (asset) return readAsset(asset);
|
|
330
|
+
return null;
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async function resolveConfined(base, confine, relative4) {
|
|
334
|
+
const target = resolve(join(base, relative4));
|
|
335
|
+
if (target !== confine && !target.startsWith(confine + sep)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
if (await isFile(target)) return target;
|
|
339
|
+
if (!extname(target)) {
|
|
340
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
341
|
+
const candidate = `${target}${ext}`;
|
|
342
|
+
if (await isFile(candidate)) return candidate;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
async function readAsset(file) {
|
|
348
|
+
const ext = extname(file).toLowerCase();
|
|
349
|
+
const isHtml = ext === ".html";
|
|
350
|
+
if (shouldTranspile(file)) {
|
|
351
|
+
const source = await readFile(file, "utf8");
|
|
352
|
+
return {
|
|
353
|
+
body: await transpile(source, file),
|
|
354
|
+
contentType: contentTypeFor(file, true),
|
|
355
|
+
isHtml: false
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (isHtml) {
|
|
359
|
+
return {
|
|
360
|
+
body: await readFile(file, "utf8"),
|
|
361
|
+
contentType: contentTypeFor(file),
|
|
362
|
+
isHtml: true
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
body: await readFile(file),
|
|
367
|
+
contentType: contentTypeFor(file),
|
|
368
|
+
isHtml: false
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/server/index.ts
|
|
373
|
+
var HTML_HEADERS = {
|
|
374
|
+
"content-type": "text/html; charset=utf-8",
|
|
375
|
+
"cache-control": "no-cache"
|
|
376
|
+
};
|
|
377
|
+
async function startDevServer(options) {
|
|
378
|
+
const { ctx, entry } = options;
|
|
379
|
+
const { paths, dev: dev2 } = ctx.config;
|
|
380
|
+
const loadAsset = createStaticLoader({
|
|
381
|
+
root: paths.root,
|
|
382
|
+
rootDir: paths.rootDir,
|
|
383
|
+
publicDir: paths.publicDir
|
|
384
|
+
});
|
|
385
|
+
const entryFile = entry?.file ?? null;
|
|
386
|
+
const app = new H3();
|
|
387
|
+
const handler = async (event) => {
|
|
388
|
+
const { pathname } = getRequestURL(event);
|
|
389
|
+
const asset = await loadAsset(pathname);
|
|
390
|
+
if (asset) {
|
|
391
|
+
if (asset.isHtml) {
|
|
392
|
+
return new Response(injectDevClient(asset.body), {
|
|
393
|
+
headers: HTML_HEADERS
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return new Response(asset.body, {
|
|
397
|
+
headers: { "content-type": asset.contentType, "cache-control": "no-cache" }
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (entryFile) {
|
|
401
|
+
const html = await readFile(entryFile, "utf8");
|
|
402
|
+
return new Response(injectDevClient(html), { headers: HTML_HEADERS });
|
|
403
|
+
}
|
|
404
|
+
return new Response(`404 Not Found: ${pathname}`, {
|
|
405
|
+
status: 404,
|
|
406
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
app.all("/", handler);
|
|
410
|
+
app.all("/**", handler);
|
|
411
|
+
const server = createServer(toNodeHandler(app));
|
|
412
|
+
const reload = createReloadSocket({ server, logger: ctx.logger });
|
|
413
|
+
const port = await listen(server, dev2.port, dev2.host);
|
|
414
|
+
const url = `http://${dev2.host}:${port}/`;
|
|
415
|
+
return {
|
|
416
|
+
url,
|
|
417
|
+
host: dev2.host,
|
|
418
|
+
port,
|
|
419
|
+
broadcastReload: () => reload.broadcastReload(),
|
|
420
|
+
async close() {
|
|
421
|
+
await reload.close();
|
|
422
|
+
await new Promise((resolveClose, reject) => {
|
|
423
|
+
server.close((err) => err ? reject(err) : resolveClose());
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function listen(server, port, host) {
|
|
429
|
+
return new Promise((resolveListen, reject) => {
|
|
430
|
+
const onError = (err) => {
|
|
431
|
+
server.removeListener("error", onError);
|
|
432
|
+
reject(
|
|
433
|
+
err.code === "EADDRINUSE" ? new Error(`Port ${port} is already in use.`) : err
|
|
434
|
+
);
|
|
435
|
+
};
|
|
436
|
+
server.once("error", onError);
|
|
437
|
+
server.listen(port, host, () => {
|
|
438
|
+
server.removeListener("error", onError);
|
|
439
|
+
const address = server.address();
|
|
440
|
+
resolveListen(
|
|
441
|
+
typeof address === "object" && address ? address.port : port
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
function contentHash(content) {
|
|
447
|
+
return createHash("sha256").update(content).digest("base64url").slice(0, 8);
|
|
448
|
+
}
|
|
449
|
+
async function emitHashedAsset(outDir, assetsDir, sourceName, content) {
|
|
450
|
+
const ext = extname(sourceName);
|
|
451
|
+
const name = basename(sourceName, ext) || "asset";
|
|
452
|
+
const fileName = `${assetsDir}/${name}-${contentHash(content)}${ext}`;
|
|
453
|
+
const target = join(outDir, fileName);
|
|
454
|
+
await mkdir(dirname(target), { recursive: true });
|
|
455
|
+
await writeFile(target, content);
|
|
456
|
+
return fileName;
|
|
457
|
+
}
|
|
458
|
+
async function copyPublicDir(publicDir, outDir) {
|
|
459
|
+
if (!await isDirectory(publicDir)) return false;
|
|
460
|
+
try {
|
|
461
|
+
await cp(publicDir, outDir, { recursive: true });
|
|
462
|
+
return true;
|
|
463
|
+
} catch (cause) {
|
|
464
|
+
throw new BuildError(
|
|
465
|
+
`Failed to copy public assets from ${publicDir}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
466
|
+
{ cause }
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/build/css.ts
|
|
472
|
+
var STYLE_RE = /\.(css|scss|sass|less)$/i;
|
|
473
|
+
var MODULE_RE = /\.module\.[^.]+$/i;
|
|
474
|
+
function stripQuery(path) {
|
|
475
|
+
return path.split("?", 1)[0] ?? path;
|
|
476
|
+
}
|
|
477
|
+
function isStyle(path) {
|
|
478
|
+
return STYLE_RE.test(stripQuery(path));
|
|
479
|
+
}
|
|
480
|
+
function isCssModule(path) {
|
|
481
|
+
return MODULE_RE.test(stripQuery(path));
|
|
482
|
+
}
|
|
483
|
+
async function loadPostcss(root) {
|
|
484
|
+
const { default: load } = await import('postcss-load-config');
|
|
485
|
+
let config;
|
|
486
|
+
try {
|
|
487
|
+
config = await load({}, root);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
const message = error instanceof Error ? error.message : "";
|
|
490
|
+
if (/No PostCSS Config found|Cannot find (module|package)/i.test(message)) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
throw new BuildError(`Failed to load PostCSS config: ${message}`, {
|
|
494
|
+
cause: error
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
const { default: postcss } = await import('postcss');
|
|
498
|
+
const processor = postcss(config.plugins);
|
|
499
|
+
return async (css, from) => {
|
|
500
|
+
const result = await processor.process(css, { ...config.options, from });
|
|
501
|
+
return result.css;
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async function importOptional(name, forExt) {
|
|
505
|
+
try {
|
|
506
|
+
return await import(name);
|
|
507
|
+
} catch {
|
|
508
|
+
throw new BuildError(
|
|
509
|
+
`"${name}" is required to compile ${forExt} files. Install it (e.g. \`pnpm add -D ${name}\`).`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function preprocess(file, ext) {
|
|
514
|
+
if (ext === ".scss" || ext === ".sass") {
|
|
515
|
+
const sass = await importOptional("sass", ".scss/.sass");
|
|
516
|
+
return sass.compile(file, { style: "expanded" }).css;
|
|
517
|
+
}
|
|
518
|
+
if (ext === ".less") {
|
|
519
|
+
const less = (await importOptional("less", ".less")).default;
|
|
520
|
+
const out = await less.render(await readFile(file, "utf8"), { filename: file });
|
|
521
|
+
return out.css;
|
|
522
|
+
}
|
|
523
|
+
return readFile(file, "utf8");
|
|
524
|
+
}
|
|
525
|
+
async function processStyle(file, options) {
|
|
526
|
+
const clean = stripQuery(file);
|
|
527
|
+
let result;
|
|
528
|
+
try {
|
|
529
|
+
result = await bundleAsync({
|
|
530
|
+
filename: clean,
|
|
531
|
+
minify: options.minify,
|
|
532
|
+
analyzeDependencies: true,
|
|
533
|
+
cssModules: isCssModule(clean),
|
|
534
|
+
resolver: {
|
|
535
|
+
read: async (filePath) => {
|
|
536
|
+
let css2 = await preprocess(filePath, extname(filePath).toLowerCase());
|
|
537
|
+
if (options.postcss) css2 = await options.postcss(css2, filePath);
|
|
538
|
+
return css2;
|
|
539
|
+
},
|
|
540
|
+
resolve: (specifier, from) => resolve(dirname(from), specifier)
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
} catch (error) {
|
|
544
|
+
throw new BuildError(
|
|
545
|
+
`Failed to process ${basename(clean)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
546
|
+
{ cause: error }
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
let css = result.code.toString();
|
|
550
|
+
for (const dep of result.dependencies ?? []) {
|
|
551
|
+
if (dep.type === "url") {
|
|
552
|
+
const fromDir = dirname(dep.loc.filePath);
|
|
553
|
+
const target = await resolveStyleUrl(dep.url, fromDir, options.paths);
|
|
554
|
+
if (target) {
|
|
555
|
+
const fileName = await emitHashedAsset(
|
|
556
|
+
options.outDir,
|
|
557
|
+
options.assetsDir,
|
|
558
|
+
basename(target),
|
|
559
|
+
await readFile(target)
|
|
560
|
+
);
|
|
561
|
+
css = css.replaceAll(dep.placeholder, `${options.base}${fileName}`);
|
|
562
|
+
} else {
|
|
563
|
+
css = css.replaceAll(dep.placeholder, dep.url);
|
|
564
|
+
}
|
|
565
|
+
} else if (dep.type === "import") {
|
|
566
|
+
css = css.replaceAll(dep.placeholder, dep.url);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const exports = result.exports ? Object.fromEntries(
|
|
570
|
+
Object.entries(result.exports).map(([key, value]) => [key, value.name])
|
|
571
|
+
) : void 0;
|
|
572
|
+
return exports ? { css, exports } : { css };
|
|
573
|
+
}
|
|
574
|
+
async function resolveStyleUrl(url, fromDir, paths) {
|
|
575
|
+
if (/^(?:[a-z]+:)?\/\//i.test(url) || /^(?:data:|#)/i.test(url) || url.startsWith("/")) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
const candidate = resolve(fromDir, url.split(/[?#]/, 1)[0] ?? url);
|
|
579
|
+
const real = await realpath(candidate).catch(() => null);
|
|
580
|
+
if (!real) return null;
|
|
581
|
+
const root = await realpath(paths.rootDir).catch(() => paths.rootDir);
|
|
582
|
+
return real === root || real.startsWith(root + sep) ? real : null;
|
|
583
|
+
}
|
|
584
|
+
function cssLoaderSnippet(href) {
|
|
585
|
+
const h = JSON.stringify(href);
|
|
586
|
+
return `(function(){try{var d=document;if(!d.querySelector('link[href='+JSON.stringify(${h})+']')){var l=d.createElement("link");l.rel="stylesheet";l.href=${h};d.head.appendChild(l);}}catch(e){}})();`;
|
|
587
|
+
}
|
|
588
|
+
function cssPlugin(options, cssByEntry) {
|
|
589
|
+
const cssByModule = /* @__PURE__ */ new Map();
|
|
590
|
+
return {
|
|
591
|
+
name: "vantris:css",
|
|
592
|
+
async load(id) {
|
|
593
|
+
const file = stripQuery(id);
|
|
594
|
+
if (!isStyle(file)) return null;
|
|
595
|
+
const { css, exports } = await processStyle(file, options);
|
|
596
|
+
cssByModule.set(id, css);
|
|
597
|
+
const code = exports ? `export default ${JSON.stringify(exports)};` : "export default {};";
|
|
598
|
+
return { code, moduleType: "js", moduleSideEffects: "no-treeshake" };
|
|
599
|
+
},
|
|
600
|
+
async generateBundle(_outputOptions, bundle2) {
|
|
601
|
+
for (const fileName of Object.keys(bundle2)) {
|
|
602
|
+
const chunk = bundle2[fileName];
|
|
603
|
+
if (!chunk || chunk.type !== "chunk") continue;
|
|
604
|
+
const css = chunk.moduleIds.filter((moduleId) => cssByModule.has(moduleId)).map((moduleId) => cssByModule.get(moduleId)).join("\n");
|
|
605
|
+
if (!css) continue;
|
|
606
|
+
const cssFile = await emitHashedAsset(
|
|
607
|
+
options.outDir,
|
|
608
|
+
options.assetsDir,
|
|
609
|
+
`${chunk.name}.css`,
|
|
610
|
+
css
|
|
611
|
+
);
|
|
612
|
+
if (chunk.isEntry) {
|
|
613
|
+
cssByEntry.set(chunk.name, cssFile);
|
|
614
|
+
} else {
|
|
615
|
+
chunk.code = cssLoaderSnippet(`${options.base}${cssFile}`) + chunk.code;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/build/bundle.ts
|
|
623
|
+
var ASSET_EXTENSION_SET = new Set(ASSET_EXTENSIONS);
|
|
624
|
+
async function bundle(input) {
|
|
625
|
+
const { entries, config } = input;
|
|
626
|
+
const { paths, build: build2 } = config;
|
|
627
|
+
const cssByEntry = /* @__PURE__ */ new Map();
|
|
628
|
+
const styleOptions = {
|
|
629
|
+
paths,
|
|
630
|
+
outDir: paths.outDir,
|
|
631
|
+
assetsDir: build2.assetsDir,
|
|
632
|
+
base: config.base,
|
|
633
|
+
minify: build2.minify,
|
|
634
|
+
postcss: input.postcss ?? null
|
|
635
|
+
};
|
|
636
|
+
const inputOptions = {
|
|
637
|
+
input: entries,
|
|
638
|
+
cwd: paths.root,
|
|
639
|
+
platform: "browser",
|
|
640
|
+
// Images/fonts/media imported from JS become hashed assets with absolute
|
|
641
|
+
// URLs from `base`; the CSS plugin handles styles (modules, url(), lazy).
|
|
642
|
+
plugins: [assetUrlPlugin(config.base), cssPlugin(styleOptions, cssByEntry)]
|
|
643
|
+
// Tree shaking is enabled by default.
|
|
644
|
+
};
|
|
645
|
+
const outputOptions = {
|
|
646
|
+
dir: paths.outDir,
|
|
647
|
+
format: "es",
|
|
648
|
+
minify: build2.minify,
|
|
649
|
+
sourcemap: build2.sourcemap,
|
|
650
|
+
entryFileNames: toChunkNames(build2.entryFileNames),
|
|
651
|
+
chunkFileNames: toChunkNames(build2.chunkFileNames),
|
|
652
|
+
assetFileNames: toAssetNames(build2.assetFileNames)
|
|
653
|
+
};
|
|
654
|
+
let result;
|
|
655
|
+
try {
|
|
656
|
+
const bundler = await rolldown(inputOptions);
|
|
657
|
+
try {
|
|
658
|
+
result = await bundler.write(outputOptions);
|
|
659
|
+
} finally {
|
|
660
|
+
await bundler.close();
|
|
661
|
+
}
|
|
662
|
+
} catch (cause) {
|
|
663
|
+
throw new BuildError(
|
|
664
|
+
`Bundling failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
665
|
+
{ cause }
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
const hasEntry = result.output.some(
|
|
669
|
+
(item) => item.type === "chunk" && item.isEntry
|
|
670
|
+
);
|
|
671
|
+
if (!hasEntry) {
|
|
672
|
+
throw new BuildError("Bundler produced no entry chunk.");
|
|
673
|
+
}
|
|
674
|
+
return { output: result.output, cssByEntry };
|
|
675
|
+
}
|
|
676
|
+
function entryFileName(output, name) {
|
|
677
|
+
const chunk = output.find(
|
|
678
|
+
(item) => item.type === "chunk" && item.isEntry && item.name === name
|
|
679
|
+
);
|
|
680
|
+
if (!chunk) {
|
|
681
|
+
throw new BuildError(`No output chunk produced for entry "${name}".`);
|
|
682
|
+
}
|
|
683
|
+
return chunk.fileName;
|
|
684
|
+
}
|
|
685
|
+
function toChunkNames(value) {
|
|
686
|
+
if (typeof value === "string") return value;
|
|
687
|
+
return (chunk) => value({
|
|
688
|
+
name: chunk.name,
|
|
689
|
+
isEntry: chunk.isEntry,
|
|
690
|
+
isDynamicEntry: chunk.isDynamicEntry,
|
|
691
|
+
facadeModuleId: chunk.facadeModuleId ?? null,
|
|
692
|
+
moduleIds: chunk.moduleIds,
|
|
693
|
+
exports: chunk.exports
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
function toAssetNames(value) {
|
|
697
|
+
if (typeof value === "string") return value;
|
|
698
|
+
return (asset) => value({
|
|
699
|
+
names: asset.names,
|
|
700
|
+
originalFileNames: asset.originalFileNames
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function assetUrlPlugin(base) {
|
|
704
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
705
|
+
return {
|
|
706
|
+
name: "vantris:asset-urls",
|
|
707
|
+
async load(id) {
|
|
708
|
+
const file = id.split("?", 1)[0] ?? id;
|
|
709
|
+
if (!ASSET_EXTENSION_SET.has(extname(file).toLowerCase())) return null;
|
|
710
|
+
const referenceId = this.emitFile({
|
|
711
|
+
type: "asset",
|
|
712
|
+
name: basename(file),
|
|
713
|
+
originalFileName: file,
|
|
714
|
+
source: await readFile(file)
|
|
715
|
+
});
|
|
716
|
+
const token = `__VANTRIS_ASSET_${referenceId}__`;
|
|
717
|
+
tokens.set(token, referenceId);
|
|
718
|
+
return `export default ${JSON.stringify(token)};`;
|
|
719
|
+
},
|
|
720
|
+
renderChunk(code) {
|
|
721
|
+
const magic = new MagicString(code);
|
|
722
|
+
let changed = false;
|
|
723
|
+
for (const [token, referenceId] of tokens) {
|
|
724
|
+
const url = `${base}${this.getFileName(referenceId)}`;
|
|
725
|
+
for (let index = code.indexOf(token); index !== -1; index = code.indexOf(token, index + token.length)) {
|
|
726
|
+
magic.update(index, index + token.length, url);
|
|
727
|
+
changed = true;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return changed ? { code: magic.toString(), map: magic.generateMap({ hires: true }) } : null;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function resolveHtmlEntries(entry, paths) {
|
|
735
|
+
if (entry.scripts.length === 0) {
|
|
736
|
+
throw new BuildError(
|
|
737
|
+
`No <script type="module" src="..."> found in ${entry.file}.`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const used = /* @__PURE__ */ new Set();
|
|
741
|
+
return entry.scripts.map((script) => {
|
|
742
|
+
const entryFile = resolve(paths.root, script.src.replace(/^\/+/, ""));
|
|
743
|
+
const base = basename(script.src).replace(/\.[^.]+$/, "") || "entry";
|
|
744
|
+
let name = base;
|
|
745
|
+
for (let i = 1; used.has(name); i++) name = `${base}${i}`;
|
|
746
|
+
used.add(name);
|
|
747
|
+
return { name, entryFile, entrySrc: script.src };
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
function renderProductionHtml(html, replacements) {
|
|
751
|
+
let out = html;
|
|
752
|
+
for (const { from, to } of replacements) out = out.replaceAll(from, to);
|
|
753
|
+
return out;
|
|
754
|
+
}
|
|
755
|
+
var HTML_URL_REF = /<(?:link|img|script|source|use)\b[^>]*?\b(?:href|src|xlink:href)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
756
|
+
function collectAssetRefs(html) {
|
|
757
|
+
const urls = /* @__PURE__ */ new Set();
|
|
758
|
+
for (const match of html.matchAll(HTML_URL_REF)) {
|
|
759
|
+
const url = match[1] ?? match[2];
|
|
760
|
+
if (url) urls.add(url);
|
|
761
|
+
}
|
|
762
|
+
return [...urls];
|
|
763
|
+
}
|
|
764
|
+
function resolveSourceRef(url, paths) {
|
|
765
|
+
if (/^(?:[a-z]+:)?\/\//i.test(url) || /^(?:data:|#|mailto:)/i.test(url)) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
const file = resolve(paths.root, url.replace(/^\/+/, ""));
|
|
769
|
+
const withinRoot = file === paths.rootDir || file.startsWith(paths.rootDir + sep);
|
|
770
|
+
return withinRoot ? file : null;
|
|
771
|
+
}
|
|
772
|
+
function injectStylesheets(html, hrefs) {
|
|
773
|
+
if (hrefs.length === 0) return html;
|
|
774
|
+
const links = hrefs.map((href) => `<link rel="stylesheet" href="${href}">`).join("\n");
|
|
775
|
+
if (html.includes("</head>")) {
|
|
776
|
+
return html.replace("</head>", `${links}
|
|
777
|
+
</head>`);
|
|
778
|
+
}
|
|
779
|
+
const bodyOpen = /<body[^>]*>/.exec(html);
|
|
780
|
+
if (bodyOpen) {
|
|
781
|
+
return html.replace(bodyOpen[0], `${bodyOpen[0]}
|
|
782
|
+
${links}`);
|
|
783
|
+
}
|
|
784
|
+
return `${links}
|
|
785
|
+
${html}`;
|
|
786
|
+
}
|
|
787
|
+
async function cleanOutDir(outDir) {
|
|
788
|
+
await rm(outDir, { recursive: true, force: true });
|
|
789
|
+
await mkdir(outDir, { recursive: true });
|
|
790
|
+
}
|
|
791
|
+
async function writeHtml(outDir, html) {
|
|
792
|
+
const file = join(outDir, HTML_ENTRY_FILENAME);
|
|
793
|
+
await writeFile(file, html, "utf8");
|
|
794
|
+
return file;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/build/index.ts
|
|
798
|
+
async function runBuild(options) {
|
|
799
|
+
const { ctx, entry } = options;
|
|
800
|
+
const { paths, build: build2 } = ctx.config;
|
|
801
|
+
const log = ctx.logger;
|
|
802
|
+
const started = Date.now();
|
|
803
|
+
const rel2 = (p) => relative(paths.root, p) || ".";
|
|
804
|
+
log.info("building for production\u2026");
|
|
805
|
+
if (!entry) {
|
|
806
|
+
throw new HtmlEntryError(
|
|
807
|
+
`No index.html found at ${paths.root}; nothing to build.`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const htmlEntries = resolveHtmlEntries(entry, paths);
|
|
811
|
+
for (const { entryFile, entrySrc } of htmlEntries) {
|
|
812
|
+
if (!await isFile(entryFile)) {
|
|
813
|
+
throw new BuildError(
|
|
814
|
+
`Entry module "${entrySrc}" resolves to a missing file: ${entryFile}`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
assertSafeOutDir(paths.outDir, paths.root, paths.rootDir, paths.publicDir);
|
|
819
|
+
log.info(`cleaning ${rel2(paths.outDir)}${sep}`);
|
|
820
|
+
await cleanOutDir(paths.outDir);
|
|
821
|
+
const inputs = Object.fromEntries(
|
|
822
|
+
htmlEntries.map((e) => [e.name, e.entryFile])
|
|
823
|
+
);
|
|
824
|
+
const postcss = await loadPostcss(paths.root);
|
|
825
|
+
log.info(
|
|
826
|
+
`bundling ${htmlEntries.length} entr${htmlEntries.length === 1 ? "y" : "ies"} with Rolldown\u2026`
|
|
827
|
+
);
|
|
828
|
+
const { output, cssByEntry } = await bundle({
|
|
829
|
+
entries: inputs,
|
|
830
|
+
config: ctx.config,
|
|
831
|
+
postcss
|
|
832
|
+
});
|
|
833
|
+
log.info(
|
|
834
|
+
`bundled ${output.length} file(s)` + (build2.minify ? " (minified)" : "") + (build2.sourcemap ? " + sourcemaps" : "")
|
|
835
|
+
);
|
|
836
|
+
if (await copyPublicDir(paths.publicDir, paths.outDir)) {
|
|
837
|
+
log.info(`copied ${rel2(paths.publicDir)}${sep} \u2192 ${rel2(paths.outDir)}${sep}`);
|
|
838
|
+
if (await isFile(join(paths.publicDir, HTML_ENTRY_FILENAME))) {
|
|
839
|
+
log.warn(
|
|
840
|
+
`public/${HTML_ENTRY_FILENAME} is ignored \u2014 the generated entry HTML takes precedence.`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const base = ctx.config.base;
|
|
845
|
+
const url = (fileName) => `${base}${fileName}`;
|
|
846
|
+
const replacements = [];
|
|
847
|
+
const entrySrcs = new Set(htmlEntries.map((e) => e.entrySrc));
|
|
848
|
+
const entries = htmlEntries.map((e) => {
|
|
849
|
+
const fileName = entryFileName(output, e.name);
|
|
850
|
+
replacements.push({ from: e.entrySrc, to: url(fileName) });
|
|
851
|
+
return { src: e.entrySrc, fileName };
|
|
852
|
+
});
|
|
853
|
+
const stylesheets = [];
|
|
854
|
+
for (const e of htmlEntries) {
|
|
855
|
+
const cssFile = cssByEntry.get(e.name);
|
|
856
|
+
if (cssFile) stylesheets.push(url(cssFile));
|
|
857
|
+
}
|
|
858
|
+
const styleOptions = {
|
|
859
|
+
paths,
|
|
860
|
+
outDir: paths.outDir,
|
|
861
|
+
assetsDir: build2.assetsDir,
|
|
862
|
+
base,
|
|
863
|
+
minify: build2.minify,
|
|
864
|
+
postcss
|
|
865
|
+
};
|
|
866
|
+
let assetCount = 0;
|
|
867
|
+
for (const ref of collectAssetRefs(entry.html)) {
|
|
868
|
+
if (entrySrcs.has(ref)) continue;
|
|
869
|
+
const file = resolveSourceRef(ref, paths);
|
|
870
|
+
if (!file || !await isFile(file)) continue;
|
|
871
|
+
let fileName;
|
|
872
|
+
if (isStyle(file)) {
|
|
873
|
+
const { css } = await processStyle(file, styleOptions);
|
|
874
|
+
const name = basename(file).replace(/\.(scss|sass|less|styl|stylus)$/i, ".css");
|
|
875
|
+
fileName = await emitHashedAsset(paths.outDir, build2.assetsDir, name, css);
|
|
876
|
+
} else {
|
|
877
|
+
fileName = await emitHashedAsset(
|
|
878
|
+
paths.outDir,
|
|
879
|
+
build2.assetsDir,
|
|
880
|
+
basename(file),
|
|
881
|
+
await readFile(file)
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
replacements.push({ from: ref, to: url(fileName) });
|
|
885
|
+
assetCount += 1;
|
|
886
|
+
}
|
|
887
|
+
let html = renderProductionHtml(entry.html, replacements);
|
|
888
|
+
html = injectStylesheets(html, stylesheets);
|
|
889
|
+
await writeHtml(paths.outDir, html);
|
|
890
|
+
if (stylesheets.length > 0) log.info(`css: ${stylesheets.length} stylesheet(s)`);
|
|
891
|
+
if (assetCount > 0) log.info(`html assets: ${assetCount} rewritten from rootDir`);
|
|
892
|
+
const durationMs = Date.now() - started;
|
|
893
|
+
const fileCount = output.length + 1 + stylesheets.length + assetCount;
|
|
894
|
+
log.info(`build complete in ${durationMs}ms \u2014 ${fileCount} files in ${rel2(paths.outDir)}${sep}`);
|
|
895
|
+
return { outDir: paths.outDir, entries, durationMs, fileCount };
|
|
896
|
+
}
|
|
897
|
+
function assertSafeOutDir(outDir, root, rootDir, publicDir) {
|
|
898
|
+
const clashes = outDir === root || outDir === rootDir || outDir === publicDir;
|
|
899
|
+
const isAncestorOfRoot = root === outDir || root.startsWith(outDir + sep);
|
|
900
|
+
if (clashes || isAncestorOfRoot) {
|
|
901
|
+
throw new BuildError(
|
|
902
|
+
`Refusing to clean outDir "${outDir}": it overlaps the project root or source directories.`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/shared/logger.ts
|
|
908
|
+
var prefix = `[${APP_NAME}]`;
|
|
909
|
+
function createLogger(options = {}) {
|
|
910
|
+
const { verbose = false, sink = console } = options;
|
|
911
|
+
return {
|
|
912
|
+
info(message) {
|
|
913
|
+
sink.log(`${prefix} ${message}`);
|
|
914
|
+
},
|
|
915
|
+
warn(message) {
|
|
916
|
+
sink.warn(`${prefix} ${message}`);
|
|
917
|
+
},
|
|
918
|
+
error(message) {
|
|
919
|
+
sink.error(`${prefix} ${message}`);
|
|
920
|
+
},
|
|
921
|
+
debug(message) {
|
|
922
|
+
if (verbose) sink.log(`${prefix} ${message}`);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/shared/context.ts
|
|
928
|
+
async function createContext(options) {
|
|
929
|
+
const { cwd, logger } = options;
|
|
930
|
+
const loaded = await loadConfig({
|
|
931
|
+
cwd,
|
|
932
|
+
logger,
|
|
933
|
+
...options.configFile !== void 0 ? { configFile: options.configFile } : {}
|
|
934
|
+
});
|
|
935
|
+
const config = resolveConfig(loaded.config, cwd, loaded.file);
|
|
936
|
+
return { cwd, config, logger };
|
|
937
|
+
}
|
|
938
|
+
function createWatcher(options) {
|
|
939
|
+
const { dir, logger, onChange } = options;
|
|
940
|
+
const watcher = watch(dir, {
|
|
941
|
+
ignoreInitial: true,
|
|
942
|
+
persistent: true,
|
|
943
|
+
ignored: (path) => path.includes("node_modules") || /(^|[/\\])\.[^/\\]/.test(path)
|
|
944
|
+
});
|
|
945
|
+
const emit = (kind) => (file) => {
|
|
946
|
+
logger.debug(`watch ${kind}: ${file}`);
|
|
947
|
+
onChange({ kind, file });
|
|
948
|
+
};
|
|
949
|
+
watcher.on("add", emit("add")).on("change", emit("change")).on("unlink", emit("unlink")).on(
|
|
950
|
+
"error",
|
|
951
|
+
(error) => logger.error(`watcher error: ${error.message}`)
|
|
952
|
+
);
|
|
953
|
+
return {
|
|
954
|
+
close: () => watcher.close()
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function rel(ctx, target) {
|
|
958
|
+
return relative(ctx.config.paths.root, target) || ".";
|
|
959
|
+
}
|
|
960
|
+
async function inspectProject(ctx) {
|
|
961
|
+
const { paths, configFile } = ctx.config;
|
|
962
|
+
ctx.logger.info(
|
|
963
|
+
`config: ${configFile ? rel(ctx, configFile) : "defaults (no config file)"}`
|
|
964
|
+
);
|
|
965
|
+
ctx.logger.info(`rootDir: ${rel(ctx, paths.rootDir)}`);
|
|
966
|
+
ctx.logger.info(`publicDir: ${rel(ctx, paths.publicDir)}`);
|
|
967
|
+
ctx.logger.info(`outDir: ${rel(ctx, paths.outDir)}`);
|
|
968
|
+
const entry = await detectHtmlEntry(paths.root);
|
|
969
|
+
if (entry) {
|
|
970
|
+
ctx.logger.info(`html entry: ${rel(ctx, entry.file)}`);
|
|
971
|
+
} else {
|
|
972
|
+
ctx.logger.warn(
|
|
973
|
+
"no index.html found at the project root; nothing to serve yet."
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
return entry;
|
|
977
|
+
}
|
|
978
|
+
async function prepareDirectories(ctx, dirs) {
|
|
979
|
+
for (const dir of dirs) {
|
|
980
|
+
await ensureDir(dir);
|
|
981
|
+
ctx.logger.debug(`prepared directory: ${dir}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/commands/dev.ts
|
|
986
|
+
var RELOAD_DEBOUNCE_MS = 50;
|
|
987
|
+
var dev = {
|
|
988
|
+
name: "dev",
|
|
989
|
+
description: "Start the development server",
|
|
990
|
+
async run(ctx) {
|
|
991
|
+
const { root, rootDir, publicDir } = ctx.config.paths;
|
|
992
|
+
await prepareDirectories(ctx, [rootDir, publicDir]);
|
|
993
|
+
const entry = await inspectProject(ctx);
|
|
994
|
+
const server = await startDevServer({ ctx, entry });
|
|
995
|
+
ctx.logger.info(`ready \u2014 dev server running at ${server.url}`);
|
|
996
|
+
let timer;
|
|
997
|
+
const watcher = createWatcher({
|
|
998
|
+
dir: rootDir,
|
|
999
|
+
logger: ctx.logger,
|
|
1000
|
+
onChange: ({ kind, file }) => {
|
|
1001
|
+
ctx.logger.info(`${kind}: ${relative(root, file)} \u2014 reloading`);
|
|
1002
|
+
if (timer) clearTimeout(timer);
|
|
1003
|
+
timer = setTimeout(() => server.broadcastReload(), RELOAD_DEBOUNCE_MS);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
await waitForShutdown();
|
|
1007
|
+
ctx.logger.info("shutting down\u2026");
|
|
1008
|
+
if (timer) clearTimeout(timer);
|
|
1009
|
+
await watcher.close();
|
|
1010
|
+
await server.close();
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
function waitForShutdown() {
|
|
1014
|
+
return new Promise((resolve5) => {
|
|
1015
|
+
const onSignal = () => {
|
|
1016
|
+
process.removeListener("SIGINT", onSignal);
|
|
1017
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1018
|
+
resolve5();
|
|
1019
|
+
};
|
|
1020
|
+
process.once("SIGINT", onSignal);
|
|
1021
|
+
process.once("SIGTERM", onSignal);
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/commands/build.ts
|
|
1026
|
+
var build = {
|
|
1027
|
+
name: "build",
|
|
1028
|
+
description: "Build the project for production",
|
|
1029
|
+
async run(ctx) {
|
|
1030
|
+
const entry = await inspectProject(ctx);
|
|
1031
|
+
await runBuild({ ctx, entry });
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// src/preview/index.ts
|
|
1036
|
+
async function runPreview(options) {
|
|
1037
|
+
const { ctx } = options;
|
|
1038
|
+
ctx.logger.info(
|
|
1039
|
+
"preview is not implemented in v0.1.0 (planned: static server over outDir)."
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/commands/preview.ts
|
|
1044
|
+
var preview = {
|
|
1045
|
+
name: "preview",
|
|
1046
|
+
description: "Locally preview a production build",
|
|
1047
|
+
async run(ctx) {
|
|
1048
|
+
await prepareDirectories(ctx, [ctx.config.paths.outDir]);
|
|
1049
|
+
await inspectProject(ctx);
|
|
1050
|
+
await runPreview({ ctx });
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
// src/commands/index.ts
|
|
1055
|
+
var commands = {
|
|
1056
|
+
[dev.name]: dev,
|
|
1057
|
+
[build.name]: build,
|
|
1058
|
+
[preview.name]: preview
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// src/cli/help.ts
|
|
1062
|
+
function helpText() {
|
|
1063
|
+
const lines = [
|
|
1064
|
+
`${APP_NAME} v${VERSION} \u2014 a modern bundler for JavaScript/TypeScript`,
|
|
1065
|
+
"",
|
|
1066
|
+
"Usage:",
|
|
1067
|
+
` ${APP_NAME} <command> [options]`,
|
|
1068
|
+
"",
|
|
1069
|
+
"Commands:"
|
|
1070
|
+
];
|
|
1071
|
+
const width = Math.max(...Object.keys(commands).map((n) => n.length));
|
|
1072
|
+
for (const command of Object.values(commands)) {
|
|
1073
|
+
lines.push(` ${command.name.padEnd(width)} ${command.description}`);
|
|
1074
|
+
}
|
|
1075
|
+
lines.push(
|
|
1076
|
+
"",
|
|
1077
|
+
"Options:",
|
|
1078
|
+
" -h, --help Show this help",
|
|
1079
|
+
" -v, --version Show the version number"
|
|
1080
|
+
);
|
|
1081
|
+
return lines.join("\n");
|
|
1082
|
+
}
|
|
1083
|
+
function versionText() {
|
|
1084
|
+
return `${APP_NAME} v${VERSION}`;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/cli/run.ts
|
|
1088
|
+
async function run(argv, options = {}) {
|
|
1089
|
+
const verbose = argv.includes("--verbose") || argv.includes("--debug");
|
|
1090
|
+
const logger = options.logger ?? createLogger({ verbose });
|
|
1091
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1092
|
+
const [first, ...rest] = argv.filter((arg) => !isGlobalFlag(arg));
|
|
1093
|
+
if (!first || first === "--help" || first === "-h" || first === "help") {
|
|
1094
|
+
logger.info(helpText());
|
|
1095
|
+
return 0 /* Ok */;
|
|
1096
|
+
}
|
|
1097
|
+
if (first === "--version" || first === "-v") {
|
|
1098
|
+
logger.info(versionText());
|
|
1099
|
+
return 0 /* Ok */;
|
|
1100
|
+
}
|
|
1101
|
+
const command = commands[first];
|
|
1102
|
+
if (!command) {
|
|
1103
|
+
logger.error(`Unknown command: "${first}"`);
|
|
1104
|
+
logger.info(helpText());
|
|
1105
|
+
return 1 /* Error */;
|
|
1106
|
+
}
|
|
1107
|
+
const ctx = await createContext({ cwd, logger });
|
|
1108
|
+
await command.run(ctx, rest);
|
|
1109
|
+
return 0 /* Ok */;
|
|
1110
|
+
}
|
|
1111
|
+
function isGlobalFlag(arg) {
|
|
1112
|
+
return arg === "--verbose" || arg === "--debug";
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export { BuildError, ConfigError, DEV_CLIENT_SCRIPT, HtmlEntryError, NotImplementedError, VERSION, VantrisError, commands, createContext, createLogger, createWatcher, detectHtmlEntry, injectDevClient, isVantrisError, loadConfig, parseHtml, resolveConfig, run, runBuild, startDevServer };
|
|
1116
|
+
//# sourceMappingURL=chunk-LRSN7SF4.js.map
|
|
1117
|
+
//# sourceMappingURL=chunk-LRSN7SF4.js.map
|