@staticbolt/core 1.0.0-beta.29 → 1.0.0-beta.30
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/lib/cli/index.mjs +45 -38
- package/lib/cli/index.mjs.map +1 -1
- package/lib/index.d.mts +91 -5
- package/lib/index.d.mts.map +1 -1
- package/lib/index.mjs +6 -0
- package/lib/index.mjs.map +1 -1
- package/lib/load-config-D-FtbUws.mjs +57 -0
- package/lib/load-config-D-FtbUws.mjs.map +1 -0
- package/lib/plugins/index.d.mts +6 -1
- package/lib/plugins/index.d.mts.map +1 -1
- package/lib/plugins/index.mjs +308 -134
- package/lib/plugins/index.mjs.map +1 -1
- package/package.json +4 -3
package/lib/plugins/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { A as isTextAssetMetadata, C as isBinaryAssetMetadata, D as isScriptMetadata, E as isPackageMetadata, F as readJsonFile, H as splitHtmlLink, L as safeReadFileSync, M as isScriptType, N as METADATA_TYPES, O as isStyleMetadata, P as Resolver, S as filterStyleMetadata, T as isMarkdownMetadata, U as DependencyTracker, V as isValidRelativePath, _ as mergeMaps, b as printFmtError, c as downloadContent, d as hashContent, f as humanReadableBytes, h as isURL, j as isWebManifestMetadata, k as isSvgMetadata, l as escapeHtml, n as bytesToKB, s as cloneObject, w as isHtmlMetadata, x as filterScriptMetadata, y as PrintFormattedError, z as valueOrError } from "../utilities-D0KXIZ-B.mjs";
|
|
2
|
-
import { _ as replaceExtension, a as basename, c as isAbsolute, d as join, f as normalize, g as relative, i as createLog, l as isPathMatch, m as parsePatterns, n as CUSTOM_ATTRIBUTES, o as dirname, p as parse$2, r as Log, s as extname,
|
|
2
|
+
import { _ as replaceExtension, a as basename, c as isAbsolute, d as join, f as normalize, g as relative, i as createLog, l as isPathMatch, m as parsePatterns, n as CUSTOM_ATTRIBUTES, o as dirname, p as parse$2, r as Log, s as extname, u as isSubpath, v as resolve } from "../common-DUFKS3lW.mjs";
|
|
3
3
|
import { _ as minifyHtml, a as formatStyle, c as minifyStylePass, d as minifyScript, f as minifyScriptPass, g as formatHtmlPass, h as formatHtml, i as minifySvgPass, l as formatScript, m as formatMarkdownPass, n as formatSvgPass, o as formatStylePass, p as formatMarkdown, r as minifySvg, s as minifyStyle, t as formatSvg, u as formatScriptPass, v as minifyHtmlPass, y as formatCode } from "../deferred-DTj91vEg.mjs";
|
|
4
|
+
import { t as loadConfigFile } from "../load-config-D-FtbUws.mjs";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
7
|
import nodePath from "node:path";
|
|
7
8
|
import chalk from "chalk";
|
|
8
9
|
import json5 from "json5";
|
|
@@ -22,6 +23,7 @@ import transformDefine from "babel-plugin-transform-define";
|
|
|
22
23
|
import { availableParallelism } from "node:os";
|
|
23
24
|
import { Worker } from "node:worker_threads";
|
|
24
25
|
import vm from "node:vm";
|
|
26
|
+
import transformModulesCommonjs from "@babel/plugin-transform-modules-commonjs";
|
|
25
27
|
import * as z$1 from "zod/v4";
|
|
26
28
|
import rehypeExternalLinks from "rehype-external-links";
|
|
27
29
|
import rehypeSlug from "rehype-slug";
|
|
@@ -3284,15 +3286,23 @@ function resolveWorkerUrl() {
|
|
|
3284
3286
|
|
|
3285
3287
|
//#endregion
|
|
3286
3288
|
//#region src/helpers/execute-js-in-vm.ts
|
|
3289
|
+
/** Runs an ES module graph inside a `node:vm` context and hands back what the entry module exported. */
|
|
3287
3290
|
async function executeJsInVmUnsafe(options) {
|
|
3288
|
-
const {
|
|
3291
|
+
const { dom, context } = await createSandbox(options.html, options.context);
|
|
3292
|
+
return {
|
|
3293
|
+
exports: { ...await new ModuleRunner(context, options).import(options.entryFile) },
|
|
3294
|
+
dom
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
/** A jsdom window when there is html to work on, a bare object with the essentials otherwise. */
|
|
3298
|
+
async function createSandbox(html, globals = {}) {
|
|
3289
3299
|
let dom;
|
|
3290
3300
|
if (html) {
|
|
3291
3301
|
const { JSDOM, VirtualConsole } = await import("jsdom");
|
|
3292
|
-
dom =
|
|
3302
|
+
dom = new JSDOM(html, {
|
|
3293
3303
|
virtualConsole: new VirtualConsole(),
|
|
3294
3304
|
url: "http://localhost/"
|
|
3295
|
-
})
|
|
3305
|
+
});
|
|
3296
3306
|
}
|
|
3297
3307
|
const window = dom ? dom.window : {};
|
|
3298
3308
|
if (!dom) Object.assign(window, {
|
|
@@ -3303,96 +3313,150 @@ async function executeJsInVmUnsafe(options) {
|
|
|
3303
3313
|
fetch,
|
|
3304
3314
|
URL
|
|
3305
3315
|
});
|
|
3306
|
-
Object.assign(window,
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3316
|
+
Object.assign(window, { console }, globals);
|
|
3317
|
+
return {
|
|
3318
|
+
dom,
|
|
3319
|
+
context: vm.createContext(window)
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
const BASE_META_URL = "http://localhost:3000";
|
|
3323
|
+
/**
|
|
3324
|
+
* Runs modules the way ES modules run: each one once, its static imports before it in source order, top-level `await` included. A
|
|
3325
|
+
* module that imports one of its own importers (a cycle) gets its exports as far as they are.
|
|
3326
|
+
*
|
|
3327
|
+
* Modules are rewritten to CommonJS and run as functions in the context, which keeps them collectable once the run is over.
|
|
3328
|
+
*/
|
|
3329
|
+
var ModuleRunner = class {
|
|
3330
|
+
/** Resolves to the module's exports once it has run. Registered before the module starts, so it only ever runs once. */
|
|
3331
|
+
#runs = /* @__PURE__ */ new Map();
|
|
3332
|
+
/** The exports of every module that has started, which is what a cycle gets to see. */
|
|
3333
|
+
#exports = /* @__PURE__ */ new Map();
|
|
3334
|
+
#context;
|
|
3335
|
+
#options;
|
|
3336
|
+
constructor(context, options) {
|
|
3337
|
+
this.#context = context;
|
|
3338
|
+
this.#options = options;
|
|
3322
3339
|
}
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
};
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
const
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
});
|
|
3358
|
-
modules[resolveName] = module;
|
|
3359
|
-
return module;
|
|
3360
|
-
}
|
|
3361
|
-
if (Object.hasOwn(modules, resolveName)) return modules[resolveName];
|
|
3362
|
-
if (isNativeModule) {
|
|
3363
|
-
const builtIn = await import(specifier);
|
|
3364
|
-
const exportNames = Object.keys(builtIn);
|
|
3365
|
-
const module = new vm.SyntheticModule(exportNames, function() {
|
|
3366
|
-
for (const key of exportNames) this.setExport(key, builtIn[key]);
|
|
3367
|
-
}, {
|
|
3368
|
-
context,
|
|
3369
|
-
identifier: resolveName
|
|
3370
|
-
});
|
|
3371
|
-
modules[resolveName] = module;
|
|
3372
|
-
return module;
|
|
3373
|
-
}
|
|
3374
|
-
const moduleScript = await options.getCode(resolveName);
|
|
3375
|
-
if (!moduleScript) {
|
|
3376
|
-
Log.error(`[executeJsInVM] can't find the script '${resolveName}' in the transformed scripts map`);
|
|
3377
|
-
throw new Error(`can't resolve module '${specifier}' from '${referencingModule.identifier}'`);
|
|
3378
|
-
}
|
|
3379
|
-
const module = new vm.SourceTextModule(moduleScript, {
|
|
3380
|
-
context: referencingModule.context,
|
|
3381
|
-
identifier: resolveName,
|
|
3382
|
-
importModuleDynamically,
|
|
3383
|
-
initializeImportMeta
|
|
3340
|
+
/**
|
|
3341
|
+
* Runs the module unless it already has, and resolves to its exports. `importers` is the chain of static imports that led here,
|
|
3342
|
+
* empty for the entry and for dynamic imports.
|
|
3343
|
+
*/
|
|
3344
|
+
import(id, importers = /* @__PURE__ */ new Set()) {
|
|
3345
|
+
let run = this.#runs.get(id);
|
|
3346
|
+
if (!run) {
|
|
3347
|
+
run = this.#run(id, importers);
|
|
3348
|
+
this.#runs.set(id, run);
|
|
3349
|
+
}
|
|
3350
|
+
return run;
|
|
3351
|
+
}
|
|
3352
|
+
async #run(id, importers) {
|
|
3353
|
+
const exports = {};
|
|
3354
|
+
this.#exports.set(id, exports);
|
|
3355
|
+
if (id.startsWith("node:")) return Object.assign(exports, await import(id));
|
|
3356
|
+
this.#options.onModuleLoad?.(id);
|
|
3357
|
+
if (extname(id) === ".json") {
|
|
3358
|
+
exports.default = this.#readJson(id);
|
|
3359
|
+
return exports;
|
|
3360
|
+
}
|
|
3361
|
+
const source = await this.#options.getCode(id);
|
|
3362
|
+
if (!source) throw new Error(`[executeJsInVM] can't find the script '${id}'`);
|
|
3363
|
+
const { code, imports } = await toModuleBody(source, id);
|
|
3364
|
+
const chain = new Set(importers).add(id);
|
|
3365
|
+
for (const specifier of imports) {
|
|
3366
|
+
const importId = this.#resolve(specifier, id);
|
|
3367
|
+
if (!chain.has(importId)) await this.import(importId, chain);
|
|
3368
|
+
}
|
|
3369
|
+
await this.#compile(code, id)({
|
|
3370
|
+
require: (specifier) => this.#require(specifier, id),
|
|
3371
|
+
__import: (specifier) => this.import(this.#resolve(specifier, id)),
|
|
3372
|
+
__meta: createImportMeta(id),
|
|
3373
|
+
exports
|
|
3384
3374
|
});
|
|
3385
|
-
|
|
3386
|
-
|
|
3375
|
+
return exports;
|
|
3376
|
+
}
|
|
3377
|
+
/** Creates the function in the context, so its globals are the sandbox's. */
|
|
3378
|
+
#compile(code, id) {
|
|
3379
|
+
return new vm.Script(`"use strict"; (async function ({ require, __import, __meta, exports }) {\n${code}\n})`, {
|
|
3380
|
+
filename: id,
|
|
3381
|
+
lineOffset: -1
|
|
3382
|
+
}).runInContext(this.#context);
|
|
3383
|
+
}
|
|
3384
|
+
#require(specifier, importer) {
|
|
3385
|
+
const exports = this.#exports.get(this.#resolve(specifier, importer));
|
|
3386
|
+
if (!exports) throw new Error(`[executeJsInVM] '${specifier}' has not run before '${importer}'`);
|
|
3387
|
+
return exports;
|
|
3387
3388
|
}
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3389
|
+
#resolve(specifier, importer) {
|
|
3390
|
+
return specifier.startsWith("node:") ? specifier : join(dirname(importer), specifier);
|
|
3391
|
+
}
|
|
3392
|
+
#readJson(id) {
|
|
3393
|
+
const jsonPath = join(this.#options.root, id);
|
|
3394
|
+
if (!existsSync(jsonPath)) throw new Error(`[executeJsInVM] can't find the file '${id}'`);
|
|
3395
|
+
return JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
3396
|
+
}
|
|
3397
|
+
};
|
|
3398
|
+
function createImportMeta(id) {
|
|
3399
|
+
if (extname(id) === ".html") id = dirname(id) + "/";
|
|
3400
|
+
const url = new URL(id, BASE_META_URL).href;
|
|
3391
3401
|
return {
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3402
|
+
url,
|
|
3403
|
+
resolve: (specifier) => new URL(specifier, url).href
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* The same module runs once per page that uses it, so its rewrite is kept by source. The oldest entries go once there are more
|
|
3408
|
+
* than `MODULE_BODY_CACHE_SIZE`, which keeps a long editing session from piling up dead ones.
|
|
3409
|
+
*/
|
|
3410
|
+
const moduleBodyCache = /* @__PURE__ */ new Map();
|
|
3411
|
+
const MODULE_BODY_CACHE_SIZE = 256;
|
|
3412
|
+
/**
|
|
3413
|
+
* Rewrites an ES module into the body of a `ModuleBody`: imports and exports become `require` and `exports`, `import.meta`
|
|
3414
|
+
* becomes `__meta` and `import()` becomes `__import()`. Also lists what the module imports statically.
|
|
3415
|
+
*/
|
|
3416
|
+
async function toModuleBody(source, id) {
|
|
3417
|
+
const cached = moduleBodyCache.get(source);
|
|
3418
|
+
if (cached) return cached;
|
|
3419
|
+
const imports = [];
|
|
3420
|
+
const result = await babel.transformAsync(source, {
|
|
3421
|
+
filename: id,
|
|
3422
|
+
sourceType: "module",
|
|
3423
|
+
babelrc: false,
|
|
3424
|
+
configFile: false,
|
|
3425
|
+
compact: false,
|
|
3426
|
+
sourceMaps: false,
|
|
3427
|
+
plugins: [collectImportsAndRewriteMeta(imports), [transformModulesCommonjs, {
|
|
3428
|
+
importInterop: "none",
|
|
3429
|
+
strictMode: false
|
|
3430
|
+
}]]
|
|
3431
|
+
});
|
|
3432
|
+
if (!result?.code) throw new Error(`[executeJsInVM] failed to transform '${id}'`);
|
|
3433
|
+
const moduleBody = {
|
|
3434
|
+
code: result.code,
|
|
3435
|
+
imports
|
|
3395
3436
|
};
|
|
3437
|
+
moduleBodyCache.set(source, moduleBody);
|
|
3438
|
+
if (moduleBodyCache.size > MODULE_BODY_CACHE_SIZE) moduleBodyCache.delete(moduleBodyCache.keys().next().value);
|
|
3439
|
+
return moduleBody;
|
|
3440
|
+
}
|
|
3441
|
+
/** Lists the static import sources into `imports`, and turns `import.meta` into `__meta` and `import()` into `__import()`. */
|
|
3442
|
+
function collectImportsAndRewriteMeta(imports) {
|
|
3443
|
+
return { visitor: {
|
|
3444
|
+
ImportDeclaration(path) {
|
|
3445
|
+
imports.push(path.node.source.value);
|
|
3446
|
+
},
|
|
3447
|
+
ExportNamedDeclaration(path) {
|
|
3448
|
+
if (path.node.source) imports.push(path.node.source.value);
|
|
3449
|
+
},
|
|
3450
|
+
ExportAllDeclaration(path) {
|
|
3451
|
+
imports.push(path.node.source.value);
|
|
3452
|
+
},
|
|
3453
|
+
MetaProperty(path) {
|
|
3454
|
+
if (path.node.meta.name === "import") path.replaceWith(t.identifier("__meta"));
|
|
3455
|
+
},
|
|
3456
|
+
CallExpression(path) {
|
|
3457
|
+
if (t.isImport(path.node.callee)) path.node.callee = t.identifier("__import");
|
|
3458
|
+
}
|
|
3459
|
+
} };
|
|
3396
3460
|
}
|
|
3397
3461
|
const executeJsInVM = valueOrError(executeJsInVmUnsafe);
|
|
3398
3462
|
|
|
@@ -3413,7 +3477,6 @@ const executeJsInVM = valueOrError(executeJsInVmUnsafe);
|
|
|
3413
3477
|
*
|
|
3414
3478
|
* Notes:
|
|
3415
3479
|
*
|
|
3416
|
-
* - Requires Node started with "--experimental-vm-modules". Without it the tags are removed without running and an error is logged.
|
|
3417
3480
|
* - "full-dom" needs a fully constructed page (a head and a body), so place this plugin last when using it.
|
|
3418
3481
|
* - Failures are per tag: the error is logged and the rest of the page carries on.
|
|
3419
3482
|
* - In development the files each page executed are tracked, so editing one of them recompiles the pages that used it.
|
|
@@ -3426,23 +3489,13 @@ function htmlBuildTimeScript(options = {}) {
|
|
|
3426
3489
|
const printFmtError = PrintFormattedError.create({ function: htmlBuildTimeScript });
|
|
3427
3490
|
/** Page → the files its build-time scripts executed. Used to recompile the page when one of them changes. */
|
|
3428
3491
|
const dependencies = new DependencyTracker();
|
|
3429
|
-
let isVmEnabled = true;
|
|
3430
3492
|
return {
|
|
3431
3493
|
name: "html-build-time-script",
|
|
3432
|
-
setup() {
|
|
3433
|
-
if (vm.SourceTextModule) return;
|
|
3434
|
-
isVmEnabled = false;
|
|
3435
|
-
printFmtError("Script execution requires the `node:vm` module, which is currently unavailable.", "\nStart Node.js with the `--experimental-vm-modules` flag to enable it.");
|
|
3436
|
-
},
|
|
3437
3494
|
async postTransform() {
|
|
3438
3495
|
const isTracking = !this.production;
|
|
3439
3496
|
const executedSources = /* @__PURE__ */ new Map();
|
|
3440
3497
|
for (const metadata of this.metadataList) {
|
|
3441
3498
|
if (!isHtmlMetadata(metadata)) continue;
|
|
3442
|
-
if (!isVmEnabled) {
|
|
3443
|
-
for (const node of metadata.ast.querySelectorAll(query)) node.remove();
|
|
3444
|
-
continue;
|
|
3445
|
-
}
|
|
3446
3499
|
const isReady = metadata.ast.querySelector("body") && metadata.ast.querySelector("head");
|
|
3447
3500
|
const scripts = metadata.ast.querySelectorAll(query);
|
|
3448
3501
|
if (scripts.length === 0) continue;
|
|
@@ -5818,7 +5871,6 @@ function htmlMergeStylesPlugin() {
|
|
|
5818
5871
|
* - A resolved link keeps the style it was written in: an extensionless or directory-style link to a page stays that way, while a
|
|
5819
5872
|
* script path resolving to ".ts", ".tsx" or ".jsx" is rewritten to ".js", which is what actually ships.
|
|
5820
5873
|
* - Query strings and hashes survive both resolving and rebasing, and a link that ends up pointing at its own page becomes "./".
|
|
5821
|
-
* - The config file is watched, but changing it only warns: it takes a restart.
|
|
5822
5874
|
*/
|
|
5823
5875
|
function coreBasePlugin() {
|
|
5824
5876
|
const JS_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -5833,16 +5885,12 @@ function coreBasePlugin() {
|
|
|
5833
5885
|
]);
|
|
5834
5886
|
return {
|
|
5835
5887
|
name: "core-base",
|
|
5836
|
-
setup() {
|
|
5837
|
-
this.watcher?.add(CONFIG_FILE_NAME);
|
|
5838
|
-
},
|
|
5839
5888
|
read(absolutePath) {
|
|
5840
5889
|
const [fileContent, readFileError] = safeReadFileSync(absolutePath, "utf8");
|
|
5841
5890
|
if (readFileError) return;
|
|
5842
5891
|
return fileContent;
|
|
5843
5892
|
},
|
|
5844
5893
|
onFileEvent(event, filePath) {
|
|
5845
|
-
if (basename(filePath) === ".staticbolt.ts") this.log.warn(`Config file "${CONFIG_FILE_NAME}" has changed. Please restart the server.`);
|
|
5846
5894
|
if (event === "unlink") this.resolver.files.delete(filePath);
|
|
5847
5895
|
},
|
|
5848
5896
|
clone(metadata) {
|
|
@@ -7691,6 +7739,8 @@ function orderByDependencyDepth(allMetadata, entryPoints, filePaths) {
|
|
|
7691
7739
|
* - On a change, the affected pages are recompiled in dependency order and metadata nothing references any more is dropped. If only
|
|
7692
7740
|
* CSS changed the stylesheets are swapped in place, otherwise the page reloads.
|
|
7693
7741
|
* - Compiled pages are trimmed to the number of connected clients, dropping the least recently served ones.
|
|
7742
|
+
* - Closing the app drops the connected clients and stops the server, which frees the port for the next one; the browser reloads
|
|
7743
|
+
* itself as soon as it reconnects.
|
|
7694
7744
|
*/
|
|
7695
7745
|
function developmentServerPlugin(options = {}) {
|
|
7696
7746
|
const host = options.host ?? "0.0.0.0";
|
|
@@ -7701,10 +7751,10 @@ function developmentServerPlugin(options = {}) {
|
|
|
7701
7751
|
let websocket;
|
|
7702
7752
|
const wsClients = /* @__PURE__ */ new Set();
|
|
7703
7753
|
const entryPointAgeMap = /* @__PURE__ */ new Map();
|
|
7704
|
-
function upgradeWebSocketHandler(request, socket, head) {
|
|
7705
|
-
|
|
7754
|
+
function upgradeWebSocketHandler(server, request, socket, head) {
|
|
7755
|
+
server.handleUpgrade(request, socket, head, (ws) => {
|
|
7706
7756
|
wsClients.add(ws);
|
|
7707
|
-
|
|
7757
|
+
server.emit("connection", ws, request);
|
|
7708
7758
|
ws.send(JSON.stringify({ data: "connected" }));
|
|
7709
7759
|
ws.on("close", () => {
|
|
7710
7760
|
wsClients.delete(ws);
|
|
@@ -7719,9 +7769,10 @@ function developmentServerPlugin(options = {}) {
|
|
|
7719
7769
|
}
|
|
7720
7770
|
return {
|
|
7721
7771
|
name: "dev-server",
|
|
7722
|
-
setup() {
|
|
7772
|
+
async setup() {
|
|
7723
7773
|
if (this.production) return;
|
|
7724
|
-
|
|
7774
|
+
const websocketServer = new WebSocketServer({ noServer: true });
|
|
7775
|
+
websocket = websocketServer;
|
|
7725
7776
|
const fastifyServe = async (request, reply) => {
|
|
7726
7777
|
const requestPath = normalize("." + decodeURIComponent(request.urlData().path || "./"));
|
|
7727
7778
|
await this.handleRequest(requestPath, reply, request);
|
|
@@ -7730,18 +7781,8 @@ function developmentServerPlugin(options = {}) {
|
|
|
7730
7781
|
fastify.register(fastifyUrlData);
|
|
7731
7782
|
fastify.register(fastifyStatic, { root: [this.root, join(this.root, publicDirectory)] });
|
|
7732
7783
|
fastify.addHook("onRequest", fastifyServe);
|
|
7733
|
-
fastify.server.on("upgrade", upgradeWebSocketHandler);
|
|
7734
|
-
|
|
7735
|
-
port,
|
|
7736
|
-
host
|
|
7737
|
-
}, (error, address) => {
|
|
7738
|
-
if (error) {
|
|
7739
|
-
this.log.error(error.message);
|
|
7740
|
-
throw new Error(error.message);
|
|
7741
|
-
}
|
|
7742
|
-
this.log.info(chalk.bold("Server listening on"), chalk.green(address + entry));
|
|
7743
|
-
});
|
|
7744
|
-
websocket.on("connection", () => {
|
|
7784
|
+
fastify.server.on("upgrade", (request, socket, head) => upgradeWebSocketHandler(websocketServer, request, socket, head));
|
|
7785
|
+
websocketServer.on("connection", () => {
|
|
7745
7786
|
const sortedByAge = Array.from(entryPointAgeMap).toSorted((a, b) => a[1] - b[1]).map(([id]) => id);
|
|
7746
7787
|
const excess = sortedByAge.length - wsClients.size;
|
|
7747
7788
|
if (excess > 0) for (const id of sortedByAge.slice(0, excess)) {
|
|
@@ -7749,6 +7790,17 @@ function developmentServerPlugin(options = {}) {
|
|
|
7749
7790
|
removeAndPruneOrphans(this.metadataList, id);
|
|
7750
7791
|
}
|
|
7751
7792
|
});
|
|
7793
|
+
const address = await fastify.listen({
|
|
7794
|
+
port,
|
|
7795
|
+
host
|
|
7796
|
+
});
|
|
7797
|
+
this.log.info(chalk.bold("Server listening on"), chalk.green(address + entry));
|
|
7798
|
+
},
|
|
7799
|
+
async teardown() {
|
|
7800
|
+
for (const client of wsClients) client.terminate();
|
|
7801
|
+
wsClients.clear();
|
|
7802
|
+
websocket?.close();
|
|
7803
|
+
await fastify?.close();
|
|
7752
7804
|
},
|
|
7753
7805
|
async onFileEvent(event, changedSource) {
|
|
7754
7806
|
if (event === "unlink") {
|
|
@@ -7903,6 +7955,7 @@ function printExecutionTime(executionTime) {
|
|
|
7903
7955
|
/** Every bound hook, in bind order. The `satisfies` makes a hook added to `Plugin` a type error until it is listed here. */
|
|
7904
7956
|
const BOUND_HOOKS = Object.keys({
|
|
7905
7957
|
setup: true,
|
|
7958
|
+
teardown: true,
|
|
7906
7959
|
read: true,
|
|
7907
7960
|
load: true,
|
|
7908
7961
|
resolveSource: true,
|
|
@@ -7947,9 +8000,10 @@ var App = class {
|
|
|
7947
8000
|
pluginData = {};
|
|
7948
8001
|
/** Exclude paths from being emitted. */
|
|
7949
8002
|
emitExclude = /* @__PURE__ */ new Set();
|
|
7950
|
-
/** Chokidar watcher. */
|
|
8003
|
+
/** Chokidar watcher. Development only, and gone once the app is closed. */
|
|
7951
8004
|
watcher;
|
|
7952
8005
|
isPluginInitialized = false;
|
|
8006
|
+
isClosed = false;
|
|
7953
8007
|
measureExecutionTime = false;
|
|
7954
8008
|
executionTime = {};
|
|
7955
8009
|
/** How long the hooks called from inside the hook currently being measured took. Held in an object so plugin contexts share it. */
|
|
@@ -7991,17 +8045,19 @@ var App = class {
|
|
|
7991
8045
|
console.error(error);
|
|
7992
8046
|
});
|
|
7993
8047
|
}
|
|
8048
|
+
/** Binds a copy of the plugin's hooks to this app, leaving the user's object untouched. */
|
|
7994
8049
|
bindPlugin(plugin, pluginIndex) {
|
|
7995
8050
|
const pluginContext = Object.create(this);
|
|
7996
8051
|
pluginContext.pluginIndex = pluginIndex;
|
|
7997
8052
|
pluginContext.pluginName = plugin.name;
|
|
7998
8053
|
pluginContext.log = createLog(`[${plugin.name}]`);
|
|
7999
|
-
const
|
|
8054
|
+
const boundPlugin = { ...plugin };
|
|
8055
|
+
const hooks = boundPlugin;
|
|
8000
8056
|
for (const hookName of BOUND_HOOKS) {
|
|
8001
8057
|
const hook = hooks[hookName];
|
|
8002
8058
|
hooks[hookName] = hook ? hook.bind(pluginContext) : emptyFunction;
|
|
8003
8059
|
}
|
|
8004
|
-
return
|
|
8060
|
+
return boundPlugin;
|
|
8005
8061
|
}
|
|
8006
8062
|
callPluginMethod(plugin, method, ...arguments_) {
|
|
8007
8063
|
if (!this.measureExecutionTime) {
|
|
@@ -8069,6 +8125,22 @@ var App = class {
|
|
|
8069
8125
|
this.log.info("Build complete");
|
|
8070
8126
|
if (this.measureExecutionTime) printExecutionTime(this.executionTime);
|
|
8071
8127
|
}
|
|
8128
|
+
/**
|
|
8129
|
+
* Stops watching and lets every plugin release what its `setup` took, in reverse order. A teardown that throws is reported and
|
|
8130
|
+
* the rest still run. Closing again does nothing.
|
|
8131
|
+
*/
|
|
8132
|
+
async close() {
|
|
8133
|
+
if (this.isClosed) return;
|
|
8134
|
+
this.isClosed = true;
|
|
8135
|
+
await this.watcher?.close();
|
|
8136
|
+
this.watcher = void 0;
|
|
8137
|
+
for (const plugin of this.plugins.toReversed()) if (plugin.teardown !== emptyFunction) try {
|
|
8138
|
+
await this.callPluginMethod(plugin, "teardown");
|
|
8139
|
+
} catch (error) {
|
|
8140
|
+
this.log.error(`[${plugin.name}] Teardown failed:`);
|
|
8141
|
+
console.error(error);
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8072
8144
|
async process(id) {
|
|
8073
8145
|
const metadata = await this.load(id);
|
|
8074
8146
|
if (!metadata) {
|
|
@@ -10119,26 +10191,128 @@ function shortenHexColor(hexColor) {
|
|
|
10119
10191
|
*
|
|
10120
10192
|
* - The command owns the run. It forces development mode onto the config and creates its own App.
|
|
10121
10193
|
* - Serving itself belongs to developmentServerPlugin; this only starts the app in the right mode.
|
|
10194
|
+
* - The config file is watched. When it changes it is loaded again, the running app is closed and a new one starts from the new
|
|
10195
|
+
* config. A config that fails to load leaves the running app alone; a new app that fails to start is closed, so nothing is
|
|
10196
|
+
* served until the next save. Plugins have to be created inside the config file: only that file is evaluated again, so plugin
|
|
10197
|
+
* objects imported from elsewhere would be the same ones the closed app used, and those are refused.
|
|
10122
10198
|
*/
|
|
10123
10199
|
function serveCliPlugin(options = {}) {
|
|
10124
10200
|
const name = options.command ?? "serve";
|
|
10125
10201
|
const aliases = options.aliases ?? ["dev"];
|
|
10126
10202
|
return {
|
|
10127
10203
|
name: "serve-cli-plugin",
|
|
10128
|
-
cli(config, configPath) {
|
|
10129
|
-
const
|
|
10204
|
+
cli(config, configPath, projectDirectory) {
|
|
10205
|
+
const serveCommand = this.defineSubcommand({
|
|
10130
10206
|
name,
|
|
10131
10207
|
aliases,
|
|
10132
|
-
meta: { description: "Dev server for
|
|
10208
|
+
meta: { description: "Dev server for staticbolt." }
|
|
10133
10209
|
});
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
await new App(config, configPath).run();
|
|
10137
|
-
});
|
|
10138
|
-
this.addCommand(buildCommand);
|
|
10210
|
+
serveCommand.onExecute(() => new DevelopmentRun(config, configPath, projectDirectory).start());
|
|
10211
|
+
this.addCommand(serveCommand);
|
|
10139
10212
|
}
|
|
10140
10213
|
};
|
|
10141
10214
|
}
|
|
10215
|
+
/** The app running in development mode, replaced by a fresh one whenever the config file changes. */
|
|
10216
|
+
var DevelopmentRun = class {
|
|
10217
|
+
#app;
|
|
10218
|
+
#config;
|
|
10219
|
+
#configPath;
|
|
10220
|
+
#configName;
|
|
10221
|
+
#projectDirectory;
|
|
10222
|
+
/** Restarts run one at a time; a change that lands during one waits its turn. */
|
|
10223
|
+
#restartQueue = Promise.resolve();
|
|
10224
|
+
/** The most recent change, so a queued restart can be skipped once a newer change is waiting behind it. */
|
|
10225
|
+
#latestChange = 0;
|
|
10226
|
+
/** When the config file was last written, as of the last time it was loaded. */
|
|
10227
|
+
#configModified;
|
|
10228
|
+
constructor(config, configPath, projectDirectory) {
|
|
10229
|
+
config.production = false;
|
|
10230
|
+
this.#config = config;
|
|
10231
|
+
this.#app = new App(config, configPath);
|
|
10232
|
+
this.#configPath = configPath;
|
|
10233
|
+
this.#configName = basename(configPath);
|
|
10234
|
+
this.#projectDirectory = projectDirectory;
|
|
10235
|
+
this.#configModified = this.#modifiedTime();
|
|
10236
|
+
}
|
|
10237
|
+
async start() {
|
|
10238
|
+
chokidar.watch(this.#configPath, {
|
|
10239
|
+
ignoreInitial: true,
|
|
10240
|
+
awaitWriteFinish: {
|
|
10241
|
+
stabilityThreshold: 100,
|
|
10242
|
+
pollInterval: 20
|
|
10243
|
+
}
|
|
10244
|
+
}).on("change", () => this.#queueRestart()).on("add", () => this.#queueRestart());
|
|
10245
|
+
this.#restartQueue = this.#app.run();
|
|
10246
|
+
await this.#restartQueue;
|
|
10247
|
+
this.#restartIfModified();
|
|
10248
|
+
}
|
|
10249
|
+
#queueRestart() {
|
|
10250
|
+
const change = ++this.#latestChange;
|
|
10251
|
+
this.#restartQueue = this.#restartQueue.then(async () => {
|
|
10252
|
+
if (change === this.#latestChange) await this.#restart();
|
|
10253
|
+
});
|
|
10254
|
+
}
|
|
10255
|
+
/**
|
|
10256
|
+
* The app starting its own watcher makes the file system drop events for a moment (libuv rebuilds its stream), so a save that
|
|
10257
|
+
* happened while the app was starting is caught by looking at the file instead.
|
|
10258
|
+
*/
|
|
10259
|
+
#restartIfModified() {
|
|
10260
|
+
if (this.#modifiedTime() !== this.#configModified) this.#queueRestart();
|
|
10261
|
+
}
|
|
10262
|
+
#modifiedTime() {
|
|
10263
|
+
return statSync(this.#configPath, { throwIfNoEntry: false })?.mtimeMs;
|
|
10264
|
+
}
|
|
10265
|
+
async #restart() {
|
|
10266
|
+
Log.info(`Config file "${this.#configName}" changed, restarting`);
|
|
10267
|
+
this.#configModified = this.#modifiedTime();
|
|
10268
|
+
const nextConfig = await this.#loadNextConfig();
|
|
10269
|
+
if (!nextConfig) return;
|
|
10270
|
+
let nextApp;
|
|
10271
|
+
try {
|
|
10272
|
+
nextApp = new App(nextConfig, this.#configPath);
|
|
10273
|
+
} catch (error) {
|
|
10274
|
+
Log.error("Failed to create the app, keeping the current server:");
|
|
10275
|
+
console.error(error);
|
|
10276
|
+
return;
|
|
10277
|
+
}
|
|
10278
|
+
try {
|
|
10279
|
+
await this.#app.close();
|
|
10280
|
+
this.#app = nextApp;
|
|
10281
|
+
this.#config = nextConfig;
|
|
10282
|
+
await nextApp.run();
|
|
10283
|
+
this.#restartIfModified();
|
|
10284
|
+
} catch (error) {
|
|
10285
|
+
Log.error(`Restart failed, nothing is served until "${this.#configName}" is fixed and saved again:`);
|
|
10286
|
+
console.error(error);
|
|
10287
|
+
await this.#app.close();
|
|
10288
|
+
}
|
|
10289
|
+
}
|
|
10290
|
+
/** The config to restart with, or nothing when the current server is to be kept. */
|
|
10291
|
+
async #loadNextConfig() {
|
|
10292
|
+
const [nextConfig, configError] = await loadConfigFile(this.#configPath, this.#projectDirectory);
|
|
10293
|
+
if (configError) {
|
|
10294
|
+
Log.error(`Failed to load "${this.#configName}", keeping the current server:`);
|
|
10295
|
+
console.error(configError);
|
|
10296
|
+
return;
|
|
10297
|
+
}
|
|
10298
|
+
const reusedPlugins = this.#reusedPlugins(nextConfig);
|
|
10299
|
+
if (reusedPlugins.length > 0) {
|
|
10300
|
+
const names = reusedPlugins.map((plugin) => `"${plugin.name}"`).join(", ");
|
|
10301
|
+
Log.error(`Keeping the current server: ${names} are the same plugin objects as before. Create plugins inside "${this.#configName}" so a reload gets fresh ones.`);
|
|
10302
|
+
return;
|
|
10303
|
+
}
|
|
10304
|
+
nextConfig.production = false;
|
|
10305
|
+
return nextConfig;
|
|
10306
|
+
}
|
|
10307
|
+
/**
|
|
10308
|
+
* A plugin object belongs to one app. Plugins imported from another file come back as the same objects, since only the config
|
|
10309
|
+
* file itself is evaluated again.
|
|
10310
|
+
*/
|
|
10311
|
+
#reusedPlugins(nextConfig) {
|
|
10312
|
+
const currentPlugins = new Set((this.#config.plugins ?? []).flat());
|
|
10313
|
+
return (nextConfig.plugins ?? []).flat().filter((plugin) => currentPlugins.has(plugin));
|
|
10314
|
+
}
|
|
10315
|
+
};
|
|
10142
10316
|
|
|
10143
10317
|
//#endregion
|
|
10144
10318
|
export { HtmlInlineSvgPlugin, analyzeOutputPlugin, buildCliPlugin, bundlePackagesPlugin, convertFontsCliPlugin, convertImagePlugin, copyAssetsPlugin, coreHtmlPlugin, coreMarkdownPlugin, coreScriptPlugin, coreStylePlugin, coreSvgPlugin, coreWebManifestPlugin, customEasePlugin, developmentServerPlugin, generateFontFacesCliPlugin, htmlBuildTimeScript, htmlBundleScriptPlugin, htmlBundleStylePlugin, htmlEnvOnlyPlugin, htmlFragmentPlugin, htmlIifeScriptPlugin, htmlInlineScriptPlugin, htmlInlineStylePlugin, htmlInlineTextPlugin, htmlInsertPlugin, htmlLayoutPlugin, htmlMarkdownPlugin, htmlMergeStylesPlugin, htmlPagesPlugin, htmlPreloadPlugin, svgoPlugin as htmlSvgoPlugin, i18nPlugin, importAsStringPlugin, loadSourcesPlugin, materialYouCliPlugin, robotsTextPlugin, serveCliPlugin, serviceWorkerPlugin, sitemapPlugin, transformCssPlugin, transformJsPlugin, webManifestPlugin, writeFilesPlugin };
|