@staticbolt/core 1.0.0-beta.30 → 1.0.0-beta.31
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/README.md +2 -1
- package/lib/cli/index.mjs +31 -8
- package/lib/cli/index.mjs.map +1 -1
- package/lib/{common-DUFKS3lW.mjs → common-D1QTZ8ra.mjs} +2 -2
- package/lib/{common-DUFKS3lW.mjs.map → common-D1QTZ8ra.mjs.map} +1 -1
- package/lib/index.d.mts +151 -3
- package/lib/index.d.mts.map +1 -1
- package/lib/index.mjs +3 -3
- package/lib/index.mjs.map +1 -1
- package/lib/{load-config-D-FtbUws.mjs → load-config-CsbiJ01A.mjs} +2 -2
- package/lib/{load-config-D-FtbUws.mjs.map → load-config-CsbiJ01A.mjs.map} +1 -1
- package/lib/plugins/index.d.mts +16 -7
- package/lib/plugins/index.d.mts.map +1 -1
- package/lib/plugins/index.mjs +739 -206
- package/lib/plugins/index.mjs.map +1 -1
- package/lib/{utilities-D0KXIZ-B.mjs → utilities-jK4uUZBV.mjs} +85 -26
- package/lib/utilities-jK4uUZBV.mjs.map +1 -0
- package/package.json +8 -4
- package/lib/utilities-D0KXIZ-B.mjs.map +0 -1
package/lib/plugins/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { A as isTextAssetMetadata, C as isBinaryAssetMetadata, D as isScriptMetadata, E as isPackageMetadata,
|
|
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-
|
|
1
|
+
import { A as isTextAssetMetadata, B as checkJsonObject, C as isBinaryAssetMetadata, D as isScriptMetadata, E as isPackageMetadata, G as isTypeScriptScript, H as isEmptyElement, I as safeReadFileSync, K as TYPESCRIPT_TYPE, M as METADATA_TYPES, N as Resolver, O as isStyleMetadata, P as readJsonFile, R as valueOrError, S as filterStyleMetadata, T as isMarkdownMetadata, U as isJavaScript, V as isDynamic, X as splitHtmlLink, Y as isValidRelativePath, Z as DependencyTracker, _ 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, q as isScriptType, s as cloneObject, w as isHtmlMetadata, x as filterScriptMetadata, y as PrintFormattedError, z as checkFileExists } from "../utilities-jK4uUZBV.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, u as isSubpath, v as resolve } from "../common-D1QTZ8ra.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-
|
|
4
|
+
import { t as loadConfigFile } from "../load-config-CsbiJ01A.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
6
|
import nodePath from "node:path";
|
|
7
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import json5 from "json5";
|
|
10
10
|
import _generator from "@babel/generator";
|
|
@@ -20,7 +20,7 @@ import * as babel from "@babel/core";
|
|
|
20
20
|
import babelPresetEnv from "@babel/preset-env";
|
|
21
21
|
import babelTypeScriptPreset from "@babel/preset-typescript";
|
|
22
22
|
import transformDefine from "babel-plugin-transform-define";
|
|
23
|
-
import { availableParallelism } from "node:os";
|
|
23
|
+
import { availableParallelism, networkInterfaces } from "node:os";
|
|
24
24
|
import { Worker } from "node:worker_threads";
|
|
25
25
|
import vm from "node:vm";
|
|
26
26
|
import transformModulesCommonjs from "@babel/plugin-transform-modules-commonjs";
|
|
@@ -41,6 +41,9 @@ import fastifyStatic from "@fastify/static";
|
|
|
41
41
|
import fastifyUrlData from "@fastify/url-data";
|
|
42
42
|
import Fastify from "fastify";
|
|
43
43
|
import { WebSocketServer } from "ws";
|
|
44
|
+
import { createServer } from "node:net";
|
|
45
|
+
import { createInterface } from "node:readline/promises";
|
|
46
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
44
47
|
import * as z from "zod";
|
|
45
48
|
import browserslistFn from "browserslist";
|
|
46
49
|
import chokidar from "chokidar";
|
|
@@ -122,7 +125,8 @@ var DependenciesFinder = class {
|
|
|
122
125
|
* - Reachability is walked from HTML pages only, so anything reachable through another entry point counts as unused.
|
|
123
126
|
* - HTML files are never listed as unused or deleted.
|
|
124
127
|
* - Empty directories under the output are removed on every run, not just when deleting unused files.
|
|
125
|
-
* - "exclude" is matched against paths relative to the output directory and applies to
|
|
128
|
+
* - "exclude" is matched against paths relative to the output directory and applies to every report, so a file a later plugin
|
|
129
|
+
* writes (a service worker, say) can be referenced without being reported missing.
|
|
126
130
|
*/
|
|
127
131
|
function analyzeOutputPlugin(options = {}) {
|
|
128
132
|
const isRemoveUnused = options.deleteUnused ?? false;
|
|
@@ -149,21 +153,21 @@ function analyzeOutputPlugin(options = {}) {
|
|
|
149
153
|
const dependencies = await dependenciesFinder.findDeps(metadataItem);
|
|
150
154
|
for (const dependency of dependencies) usedFiles.add(dependency);
|
|
151
155
|
}
|
|
152
|
-
const
|
|
156
|
+
const isExcluded = (file) => isPathMatch(relative(this.outdir, file), {
|
|
157
|
+
include: exclude,
|
|
158
|
+
root: this.outdir
|
|
159
|
+
});
|
|
160
|
+
const missingFiles = [...usedFiles.difference(availableFiles)].filter((file) => !isExcluded(file));
|
|
153
161
|
let missingFilesLog = chalk.bold("The following files are missing:");
|
|
154
162
|
for (const missing of missingFiles) missingFilesLog += "\n" + chalk.red("File not found: ") + chalk.yellow(relative(this.root, missing));
|
|
155
|
-
if (missingFiles.
|
|
163
|
+
if (missingFiles.length > 0) this.log.error(missingFilesLog);
|
|
156
164
|
const unusedFiles = availableFiles.difference(usedFiles);
|
|
157
165
|
const unusedFilesLog = [];
|
|
158
166
|
for (const unused of unusedFiles) {
|
|
159
167
|
if (extname(unused) === ".html") continue;
|
|
160
|
-
|
|
161
|
-
if (isPathMatch(relativePath, {
|
|
162
|
-
include: exclude,
|
|
163
|
-
root: this.outdir
|
|
164
|
-
})) continue;
|
|
168
|
+
if (isExcluded(unused)) continue;
|
|
165
169
|
if (isRemoveUnused) unlinkSync(unused);
|
|
166
|
-
unusedFilesLog.push(chalk.yellow(join(basename(this.outdir),
|
|
170
|
+
unusedFilesLog.push(chalk.yellow(join(basename(this.outdir), relative(this.outdir, unused))));
|
|
167
171
|
}
|
|
168
172
|
const logTitle = isRemoveUnused ? chalk.bold(chalk.red("Removed"), "(" + chalk.yellow(unusedFilesLog.length) + ")", "unused files from the output directory") : chalk.bold("Found", "(" + chalk.yellow(unusedFilesLog.length) + ")", "unused files");
|
|
169
173
|
if (unusedFilesLog.length > 0) this.log.warn(logTitle + (isLogUnusedFiles ? "\n" + unusedFilesLog.join("\n") : ""));
|
|
@@ -3460,6 +3464,54 @@ function collectImportsAndRewriteMeta(imports) {
|
|
|
3460
3464
|
}
|
|
3461
3465
|
const executeJsInVM = valueOrError(executeJsInVmUnsafe);
|
|
3462
3466
|
|
|
3467
|
+
//#endregion
|
|
3468
|
+
//#region src/plugins/html-build-time-script/lsp.ts
|
|
3469
|
+
/** What the sandbox gives every build-time script; these shadow the DOM globals of the same name. */
|
|
3470
|
+
const GLOBALS = `declare const StaticBolt: import("@staticbolt/core").App;
|
|
3471
|
+
declare const window: typeof globalThis;
|
|
3472
|
+
declare const __filepath: string;
|
|
3473
|
+
declare const __id: string;`;
|
|
3474
|
+
/** The page's tree is the parser's, not the DOM's: its node types take the DOM names, so the code reads as it runs. */
|
|
3475
|
+
const PARSER_TYPES = [
|
|
3476
|
+
"Document",
|
|
3477
|
+
"HTMLElement",
|
|
3478
|
+
"Node",
|
|
3479
|
+
"TextNode",
|
|
3480
|
+
"CommentNode",
|
|
3481
|
+
"NodeType"
|
|
3482
|
+
].map((name) => `type ${name} = import("@staticbolt/node-html-parser").${name};`).join("\n");
|
|
3483
|
+
/**
|
|
3484
|
+
* The bodies of the build-time scripts marked `type="application/x-typescript"`, served as TypeScript that sees the sandbox
|
|
3485
|
+
* globals: completion, hover and type errors. Claimed before the core plugin, later in the config, sees the browser in them.
|
|
3486
|
+
*/
|
|
3487
|
+
function embeddedLanguages$2(names) {
|
|
3488
|
+
return [{
|
|
3489
|
+
name: "build-time",
|
|
3490
|
+
filter: () => true,
|
|
3491
|
+
findRegions: (document) => buildTimeScripts(document, names).flatMap((script) => script.contentRange ? {
|
|
3492
|
+
...script.contentRange,
|
|
3493
|
+
isModule: true
|
|
3494
|
+
} : []),
|
|
3495
|
+
isColouredByEditor: true,
|
|
3496
|
+
prelude: (document) => prelude(buildTimeScripts(document, names), names)
|
|
3497
|
+
}];
|
|
3498
|
+
}
|
|
3499
|
+
/** The build-time scripts of a document that are marked as TypeScript. */
|
|
3500
|
+
function buildTimeScripts(document, names) {
|
|
3501
|
+
return document.select("script").filter((script) => script.has(names.build) && isTypeScriptScript(script));
|
|
3502
|
+
}
|
|
3503
|
+
/**
|
|
3504
|
+
* The declarations for a document's build-time scripts: the sandbox globals, and `document` as the parser's tree or, with a full
|
|
3505
|
+
* DOM, the DOM's.
|
|
3506
|
+
*/
|
|
3507
|
+
function prelude(scripts, names) {
|
|
3508
|
+
const hasFullDom = scripts.some((script) => script.has(names.fullDom));
|
|
3509
|
+
const lines = [GLOBALS];
|
|
3510
|
+
if (!hasFullDom) lines.push(PARSER_TYPES);
|
|
3511
|
+
lines.push("declare const document: Document;");
|
|
3512
|
+
return lines.join("\n");
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3463
3515
|
//#endregion
|
|
3464
3516
|
//#region src/plugins/html-build-time-script/html-build-time-script.ts
|
|
3465
3517
|
/**
|
|
@@ -3580,6 +3632,13 @@ function htmlBuildTimeScript(options = {}) {
|
|
|
3580
3632
|
if (event !== "change") return;
|
|
3581
3633
|
for (const id of Array.from(compileSet)) for (const importer of dependencies.getImporters(id)) compileSet.add(importer);
|
|
3582
3634
|
},
|
|
3635
|
+
lspEmbeddedLanguages() {
|
|
3636
|
+
return embeddedLanguages$2({
|
|
3637
|
+
build: buildAttribute,
|
|
3638
|
+
fullDom: fullDomAttribute,
|
|
3639
|
+
context: contextAttribute
|
|
3640
|
+
});
|
|
3641
|
+
},
|
|
3583
3642
|
lspHtmlData() {
|
|
3584
3643
|
return {
|
|
3585
3644
|
version: 1.1,
|
|
@@ -3589,7 +3648,7 @@ function htmlBuildTimeScript(options = {}) {
|
|
|
3589
3648
|
attributes: [
|
|
3590
3649
|
{
|
|
3591
3650
|
name: buildAttribute,
|
|
3592
|
-
description: "This attribute is used to mark the script as build-time script.",
|
|
3651
|
+
description: "This attribute is used to mark the script as build-time script.\n\nWith `type=\"application/x-typescript\"` the staticbolt language server serves the script with the sandbox globals (`StaticBolt`, `document`, `window`, `__filepath`, `__id`) in place of the browser's.",
|
|
3593
3652
|
valueSet: "v"
|
|
3594
3653
|
},
|
|
3595
3654
|
{
|
|
@@ -3604,6 +3663,25 @@ function htmlBuildTimeScript(options = {}) {
|
|
|
3604
3663
|
]
|
|
3605
3664
|
}]
|
|
3606
3665
|
};
|
|
3666
|
+
},
|
|
3667
|
+
lspValidate(document, report) {
|
|
3668
|
+
const scripts = document.select("script");
|
|
3669
|
+
for (const element of scripts) {
|
|
3670
|
+
const build = element.attribute(buildAttribute);
|
|
3671
|
+
if (!build) continue;
|
|
3672
|
+
if (!isJavaScript(element)) {
|
|
3673
|
+
report.warn(build.nameRange, `"${buildAttribute}" does nothing on a script that is not JavaScript`);
|
|
3674
|
+
continue;
|
|
3675
|
+
}
|
|
3676
|
+
const context = element.attribute(contextAttribute);
|
|
3677
|
+
if (context) checkJsonObject(context, report);
|
|
3678
|
+
const source = element.attribute("src");
|
|
3679
|
+
if (!source?.value && isEmptyElement(element, document)) {
|
|
3680
|
+
report.error(build.nameRange, `"${buildAttribute}" needs a "src" or some code in the script to run`);
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3683
|
+
if (source) checkFileExists(source, document, report);
|
|
3684
|
+
}
|
|
3607
3685
|
}
|
|
3608
3686
|
};
|
|
3609
3687
|
}
|
|
@@ -3884,6 +3962,23 @@ function htmlBundleScriptPlugin(options = {}) {
|
|
|
3884
3962
|
]
|
|
3885
3963
|
}]
|
|
3886
3964
|
};
|
|
3965
|
+
},
|
|
3966
|
+
lspValidate(document, report) {
|
|
3967
|
+
const scripts = document.select("script");
|
|
3968
|
+
for (const element of scripts) {
|
|
3969
|
+
const bundle = element.attribute(bundleAttribute);
|
|
3970
|
+
if (!bundle) continue;
|
|
3971
|
+
if (!isJavaScript(element)) {
|
|
3972
|
+
report.warn(bundle.nameRange, `"${bundleAttribute}" does nothing on a script that is not JavaScript`);
|
|
3973
|
+
continue;
|
|
3974
|
+
}
|
|
3975
|
+
const source = element.attribute("src");
|
|
3976
|
+
if (!source?.value && isEmptyElement(element, document)) {
|
|
3977
|
+
report.error(bundle.nameRange, `"${bundleAttribute}" needs a "src" or some code in the script to bundle`);
|
|
3978
|
+
continue;
|
|
3979
|
+
}
|
|
3980
|
+
if (source) checkFileExists(source, document, report);
|
|
3981
|
+
}
|
|
3887
3982
|
}
|
|
3888
3983
|
};
|
|
3889
3984
|
}
|
|
@@ -4472,7 +4567,7 @@ function htmlBundleStylePlugin(options = {}) {
|
|
|
4472
4567
|
}
|
|
4473
4568
|
|
|
4474
4569
|
//#endregion
|
|
4475
|
-
//#region src/plugins/html-env-only/html-
|
|
4570
|
+
//#region src/plugins/html-env-only/html-environment-only-plugin.ts
|
|
4476
4571
|
/**
|
|
4477
4572
|
* Removes the HTML tags that do not belong in the current environment.
|
|
4478
4573
|
*
|
|
@@ -4487,15 +4582,15 @@ function htmlBundleStylePlugin(options = {}) {
|
|
|
4487
4582
|
* - Where the tag survives, the marker attribute is stripped from the output.
|
|
4488
4583
|
*/
|
|
4489
4584
|
function htmlEnvOnlyPlugin(options = {}) {
|
|
4490
|
-
const
|
|
4585
|
+
const developmentOnlyAttribute = options.devOnlyAttribute ?? "dev-only";
|
|
4491
4586
|
const productionOnlyAttribute = options.productionOnlyAttribute ?? "production-only";
|
|
4492
4587
|
return {
|
|
4493
4588
|
name: "html-env-only",
|
|
4494
4589
|
transform(metadata) {
|
|
4495
4590
|
if (!isHtmlMetadata(metadata)) return;
|
|
4496
|
-
const
|
|
4497
|
-
for (const node of
|
|
4498
|
-
else node.removeAttribute(
|
|
4591
|
+
const developmentOnlyElements = metadata.ast.querySelectorAll(`[${developmentOnlyAttribute}]`);
|
|
4592
|
+
for (const node of developmentOnlyElements) if (this.production) node.remove();
|
|
4593
|
+
else node.removeAttribute(developmentOnlyAttribute);
|
|
4499
4594
|
const productionOnlyElements = metadata.ast.querySelectorAll(`[${productionOnlyAttribute}]`);
|
|
4500
4595
|
for (const node of productionOnlyElements) if (this.production) node.removeAttribute(productionOnlyAttribute);
|
|
4501
4596
|
else node.remove();
|
|
@@ -4504,7 +4599,7 @@ function htmlEnvOnlyPlugin(options = {}) {
|
|
|
4504
4599
|
return {
|
|
4505
4600
|
version: 1.1,
|
|
4506
4601
|
globalAttributes: [{
|
|
4507
|
-
name:
|
|
4602
|
+
name: developmentOnlyAttribute,
|
|
4508
4603
|
description: "Keep this tag during dev serve only; it is removed at build time.",
|
|
4509
4604
|
valueSet: "v"
|
|
4510
4605
|
}, {
|
|
@@ -4666,6 +4761,18 @@ function htmlIifeScriptPlugin(options = {}) {
|
|
|
4666
4761
|
}]
|
|
4667
4762
|
}]
|
|
4668
4763
|
};
|
|
4764
|
+
},
|
|
4765
|
+
lspValidate(document, report) {
|
|
4766
|
+
const scripts = document.select("script");
|
|
4767
|
+
for (const element of scripts) {
|
|
4768
|
+
const iife = element.attribute(iifeAttribute);
|
|
4769
|
+
if (!iife) continue;
|
|
4770
|
+
if (!isJavaScript(element)) {
|
|
4771
|
+
report.warn(iife.nameRange, `"${iifeAttribute}" does nothing on a script that is not JavaScript`);
|
|
4772
|
+
continue;
|
|
4773
|
+
}
|
|
4774
|
+
if (element.attribute("type")?.value === "module") report.warn(iife.nameRange, `"${iifeAttribute}" is for global scripts, a module already has its own scope`);
|
|
4775
|
+
}
|
|
4669
4776
|
}
|
|
4670
4777
|
};
|
|
4671
4778
|
}
|
|
@@ -4743,6 +4850,24 @@ function htmlInlineScriptPlugin(options = {}) {
|
|
|
4743
4850
|
}]
|
|
4744
4851
|
}]
|
|
4745
4852
|
};
|
|
4853
|
+
},
|
|
4854
|
+
lspValidate(document, report) {
|
|
4855
|
+
const scripts = document.select("script");
|
|
4856
|
+
for (const element of scripts) {
|
|
4857
|
+
const inline = element.attribute(inlineAttribute);
|
|
4858
|
+
if (!inline) continue;
|
|
4859
|
+
if (!isJavaScript(element)) {
|
|
4860
|
+
report.warn(inline.nameRange, `"${inlineAttribute}" does nothing on a script that is not JavaScript`);
|
|
4861
|
+
continue;
|
|
4862
|
+
}
|
|
4863
|
+
const source = element.attribute("src");
|
|
4864
|
+
if (!source?.value) {
|
|
4865
|
+
report.error(inline.nameRange, `"${inlineAttribute}" needs a "src" pointing at the script to inline`);
|
|
4866
|
+
continue;
|
|
4867
|
+
}
|
|
4868
|
+
if (!isEmptyElement(element, document)) report.error(inline.nameRange, `"${inlineAttribute}" needs an empty script tag, the content comes from "src"`);
|
|
4869
|
+
checkFileExists(source, document, report);
|
|
4870
|
+
}
|
|
4746
4871
|
}
|
|
4747
4872
|
};
|
|
4748
4873
|
}
|
|
@@ -4837,6 +4962,23 @@ function htmlInlineStylePlugin(options = {}) {
|
|
|
4837
4962
|
}]
|
|
4838
4963
|
}]
|
|
4839
4964
|
};
|
|
4965
|
+
},
|
|
4966
|
+
lspValidate(document, report) {
|
|
4967
|
+
const links = document.select("link");
|
|
4968
|
+
for (const element of links) {
|
|
4969
|
+
const inline = element.attribute(inlineAttribute);
|
|
4970
|
+
if (!inline) continue;
|
|
4971
|
+
if (element.attribute("rel")?.value !== "stylesheet") {
|
|
4972
|
+
report.warn(inline.nameRange, `"${inlineAttribute}" only works on a link with rel="stylesheet"`);
|
|
4973
|
+
continue;
|
|
4974
|
+
}
|
|
4975
|
+
const source = element.attribute("href");
|
|
4976
|
+
if (!source?.value) {
|
|
4977
|
+
report.error(inline.nameRange, `"${inlineAttribute}" needs an "href" pointing at the stylesheet to inline`);
|
|
4978
|
+
continue;
|
|
4979
|
+
}
|
|
4980
|
+
checkFileExists(source, document, report);
|
|
4981
|
+
}
|
|
4840
4982
|
}
|
|
4841
4983
|
};
|
|
4842
4984
|
}
|
|
@@ -4940,6 +5082,19 @@ function HtmlInlineSvgPlugin(options = {}) {
|
|
|
4940
5082
|
}]
|
|
4941
5083
|
}]
|
|
4942
5084
|
};
|
|
5085
|
+
},
|
|
5086
|
+
lspValidate(document, report) {
|
|
5087
|
+
const svgs = document.select("svg");
|
|
5088
|
+
for (const element of svgs) {
|
|
5089
|
+
const inline = element.attribute(inlineAttribute);
|
|
5090
|
+
if (!inline) continue;
|
|
5091
|
+
const source = element.attribute(sourceAttribute);
|
|
5092
|
+
if (!source?.value) {
|
|
5093
|
+
report.error(inline.nameRange, `"${inlineAttribute}" needs a "${sourceAttribute}" pointing at the SVG to inline`);
|
|
5094
|
+
continue;
|
|
5095
|
+
}
|
|
5096
|
+
checkFileExists(source, document, report);
|
|
5097
|
+
}
|
|
4943
5098
|
}
|
|
4944
5099
|
};
|
|
4945
5100
|
}
|
|
@@ -5083,12 +5238,30 @@ function htmlInlineTextPlugin(options = {}) {
|
|
|
5083
5238
|
}]
|
|
5084
5239
|
}]
|
|
5085
5240
|
};
|
|
5241
|
+
},
|
|
5242
|
+
lspValidate(document, report) {
|
|
5243
|
+
const inlineTags = document.select(tag);
|
|
5244
|
+
for (const element of inlineTags) {
|
|
5245
|
+
const source = element.attribute(sourceAttribute);
|
|
5246
|
+
if (!source?.value) {
|
|
5247
|
+
report.error(element, `<${tag}> needs a "${sourceAttribute}" pointing at a text file`);
|
|
5248
|
+
continue;
|
|
5249
|
+
}
|
|
5250
|
+
checkFileExists(source, document, report);
|
|
5251
|
+
}
|
|
5086
5252
|
}
|
|
5087
5253
|
};
|
|
5088
5254
|
}
|
|
5089
5255
|
|
|
5090
5256
|
//#endregion
|
|
5091
5257
|
//#region src/plugins/html-insert/html-insert-plugin.ts
|
|
5258
|
+
/** Where `insertAdjacentHTML` can put content. */
|
|
5259
|
+
const POSITIONS = /* @__PURE__ */ new Set([
|
|
5260
|
+
"beforebegin",
|
|
5261
|
+
"afterbegin",
|
|
5262
|
+
"beforeend",
|
|
5263
|
+
"afterend"
|
|
5264
|
+
]);
|
|
5092
5265
|
/**
|
|
5093
5266
|
* Moves the content of an "<insert>" tag into any element on the same page, addressed by a CSS selector, then removes the tag
|
|
5094
5267
|
* itself (names configurable via HtmlInsertOptions).
|
|
@@ -5178,28 +5351,53 @@ function htmlInsertPlugin(options = {}) {
|
|
|
5178
5351
|
{
|
|
5179
5352
|
name: whereAttribute,
|
|
5180
5353
|
description: "The position of the inserted element.",
|
|
5181
|
-
values: [
|
|
5182
|
-
{ name: "beforebegin" },
|
|
5183
|
-
{ name: "afterbegin" },
|
|
5184
|
-
{ name: "beforeend" },
|
|
5185
|
-
{ name: "afterend" }
|
|
5186
|
-
]
|
|
5354
|
+
values: [...POSITIONS].map((position) => ({ name: position }))
|
|
5187
5355
|
}
|
|
5188
5356
|
]
|
|
5189
5357
|
}]
|
|
5190
5358
|
};
|
|
5359
|
+
},
|
|
5360
|
+
lspValidate(document, report) {
|
|
5361
|
+
const inserts = document.select(tag);
|
|
5362
|
+
for (const element of inserts) {
|
|
5363
|
+
const selector = element.attribute(selectorAttribute);
|
|
5364
|
+
const where = element.attribute(whereAttribute);
|
|
5365
|
+
if (isEmptyElement(element, document)) report.error(element, `<${tag}> has nothing to insert`);
|
|
5366
|
+
if (!selector?.value) report.error(element, `<${tag}> needs a "${selectorAttribute}" with the CSS selector of the target element`);
|
|
5367
|
+
if (where?.value && !isDynamic(where.value) && !POSITIONS.has(where.value)) report.error(where, `"${whereAttribute}" must be one of ${[...POSITIONS].join(", ")}`);
|
|
5368
|
+
if (where && element.has(replaceAttribute)) report.warn(where.nameRange, `"${whereAttribute}" does nothing with "${replaceAttribute}"`);
|
|
5369
|
+
}
|
|
5191
5370
|
}
|
|
5192
5371
|
};
|
|
5193
5372
|
}
|
|
5194
5373
|
|
|
5195
5374
|
//#endregion
|
|
5196
5375
|
//#region src/plugins/html-layout/fill-placeholders.ts
|
|
5197
|
-
/**
|
|
5198
|
-
const placeholderRe$2 = /(\\)?{{\s*([\s\S]*?)\s*}}/g;
|
|
5376
|
+
/** `{{ expression }}`, or `\{{ … }}` to have it left alone. The expression is captured without its surrounding whitespace. */
|
|
5377
|
+
const placeholderRe$2 = /(\\)?{{(\s*)([\s\S]*?)\s*}}/g;
|
|
5378
|
+
/** Where the expressions of the unescaped placeholders sit in `html`. */
|
|
5379
|
+
function findPlaceholders(html) {
|
|
5380
|
+
const regions = [];
|
|
5381
|
+
for (const match of html.matchAll(placeholderRe$2)) {
|
|
5382
|
+
const [, escape, leadingSpace, expression] = match;
|
|
5383
|
+
if (escape) continue;
|
|
5384
|
+
const start = match.index + 2 + leadingSpace.length;
|
|
5385
|
+
const extent = {
|
|
5386
|
+
start: match.index,
|
|
5387
|
+
end: match.index + match[0].length
|
|
5388
|
+
};
|
|
5389
|
+
regions.push({
|
|
5390
|
+
start,
|
|
5391
|
+
end: start + expression.length,
|
|
5392
|
+
extent
|
|
5393
|
+
});
|
|
5394
|
+
}
|
|
5395
|
+
return regions;
|
|
5396
|
+
}
|
|
5199
5397
|
/** `{{ JavaScript expression }}` */
|
|
5200
5398
|
function fillLayoutPlaceholders(html, placeholders) {
|
|
5201
5399
|
const errors = [];
|
|
5202
|
-
const result = html.replace(placeholderRe$2, (match, escape, content) => {
|
|
5400
|
+
const result = html.replace(placeholderRe$2, (match, escape, _leadingSpace, content) => {
|
|
5203
5401
|
if (escape === "\\") return match.slice(1);
|
|
5204
5402
|
const [expression, executeError] = executeExpression$1(content, placeholders);
|
|
5205
5403
|
if (executeError) {
|
|
@@ -5232,6 +5430,255 @@ function executeExpression$1(expressionString, data) {
|
|
|
5232
5430
|
}
|
|
5233
5431
|
}
|
|
5234
5432
|
|
|
5433
|
+
//#endregion
|
|
5434
|
+
//#region src/plugins/html-layout/inline-layout.ts
|
|
5435
|
+
/** Turns every `add-attr-x` and `remove-attr-x` of a layout into adding or removing `x`, by the value each holds. */
|
|
5436
|
+
function applyConditionalAttributes(layout, names) {
|
|
5437
|
+
const addPrefix = `${names.addAttribute}-`;
|
|
5438
|
+
const removePrefix = `${names.removeAttribute}-`;
|
|
5439
|
+
const elements = layout.ast.querySelectorAll("*");
|
|
5440
|
+
for (const element of elements) {
|
|
5441
|
+
if (!element.attributes) continue;
|
|
5442
|
+
for (const [name, value] of Object.entries(element.attributes)) {
|
|
5443
|
+
if (name.startsWith(addPrefix)) {
|
|
5444
|
+
element.removeAttribute(name);
|
|
5445
|
+
if (isSwitchedOn(value)) element.setAttribute(name.slice(addPrefix.length), "");
|
|
5446
|
+
continue;
|
|
5447
|
+
}
|
|
5448
|
+
if (!name.startsWith(removePrefix)) continue;
|
|
5449
|
+
element.removeAttribute(name);
|
|
5450
|
+
if (isSwitchedOn(value)) element.removeAttribute(name.slice(removePrefix.length));
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
/**
|
|
5455
|
+
* Drops from a layout instance the scripts, styles and stylesheet links the page already has from an earlier instance, so a
|
|
5456
|
+
* layout used twice emits each once. A tag marked always-include is kept, and the mark removed.
|
|
5457
|
+
*/
|
|
5458
|
+
function dropRepeatedAssets(page, layout, alwaysInclude) {
|
|
5459
|
+
const scriptsAndStyles = layout.ast.querySelectorAll("script,style");
|
|
5460
|
+
for (const scriptOrStyle of scriptsAndStyles) {
|
|
5461
|
+
const id = scriptOrStyle.getAttribute(CUSTOM_ATTRIBUTES.MetadataID);
|
|
5462
|
+
if (!id) continue;
|
|
5463
|
+
if (scriptOrStyle.hasAttribute(alwaysInclude)) {
|
|
5464
|
+
scriptOrStyle.removeAttribute(alwaysInclude);
|
|
5465
|
+
continue;
|
|
5466
|
+
}
|
|
5467
|
+
const list = scriptOrStyle.tagName === "SCRIPT" ? "scriptsMetadataList" : "stylesMetadataList";
|
|
5468
|
+
if (!page[list].has(id)) continue;
|
|
5469
|
+
layout[list].delete(id);
|
|
5470
|
+
scriptOrStyle.remove();
|
|
5471
|
+
}
|
|
5472
|
+
const scriptsAndLinks = layout.ast.querySelectorAll("script[src], link[rel=\"stylesheet\"]");
|
|
5473
|
+
for (const scriptOrLink of scriptsAndLinks) {
|
|
5474
|
+
if (scriptOrLink.hasAttribute(alwaysInclude)) {
|
|
5475
|
+
scriptOrLink.removeAttribute(alwaysInclude);
|
|
5476
|
+
continue;
|
|
5477
|
+
}
|
|
5478
|
+
const isScript = scriptOrLink.tagName === "SCRIPT";
|
|
5479
|
+
const attribute = isScript ? "src" : "href";
|
|
5480
|
+
const selector = isScript ? "script[src]" : "link[rel=\"stylesheet\"]";
|
|
5481
|
+
const layoutSource = join(dirname(layout.filePath), scriptOrLink.attributes[attribute]);
|
|
5482
|
+
if (page.ast.querySelectorAll(selector).some((element) => {
|
|
5483
|
+
return join(dirname(page.filePath), element.attributes[attribute]) === layoutSource;
|
|
5484
|
+
})) scriptOrLink.remove();
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
/**
|
|
5488
|
+
* Puts the tag's children into the layout's slots, by the `slot` attribute they name or into the unnamed slots, then unwraps
|
|
5489
|
+
* every slot. A slot keeps its own content as the fallback: the first fill replaces it, later ones append.
|
|
5490
|
+
*/
|
|
5491
|
+
function fillSlots(tag, layout, layoutPath, printError) {
|
|
5492
|
+
const filledSlots = /* @__PURE__ */ new Set();
|
|
5493
|
+
const fillSlot = (slot, html) => {
|
|
5494
|
+
if (!filledSlots.has(slot)) {
|
|
5495
|
+
filledSlots.add(slot);
|
|
5496
|
+
slot.innerHTML = "";
|
|
5497
|
+
}
|
|
5498
|
+
slot.insertAdjacentHTML("beforeend", html);
|
|
5499
|
+
};
|
|
5500
|
+
const children = tag.querySelectorAll(":scope > *");
|
|
5501
|
+
for (const element of children) {
|
|
5502
|
+
if (element.nodeType !== NodeType.ELEMENT_NODE) continue;
|
|
5503
|
+
if (!element.hasAttribute("slot")) {
|
|
5504
|
+
const defaultSlots = layout.ast.querySelectorAll("slot:not([name])");
|
|
5505
|
+
if (defaultSlots.length === 0) {
|
|
5506
|
+
printError("No default slot in layout file:", layoutPath, { node: element });
|
|
5507
|
+
continue;
|
|
5508
|
+
}
|
|
5509
|
+
for (const defaultSlot of defaultSlots) fillSlot(defaultSlot, element.outerHTML);
|
|
5510
|
+
continue;
|
|
5511
|
+
}
|
|
5512
|
+
const slotName = element.getAttribute("slot");
|
|
5513
|
+
if (!slotName) continue;
|
|
5514
|
+
const namedSlots = layout.ast.querySelectorAll(`slot[name="${slotName}"]`);
|
|
5515
|
+
if (namedSlots.length === 0) {
|
|
5516
|
+
printError(`No slots with name "${slotName}" in layout file:`, layoutPath, { node: element });
|
|
5517
|
+
continue;
|
|
5518
|
+
}
|
|
5519
|
+
element.removeAttribute("slot");
|
|
5520
|
+
for (const namedSlot of namedSlots) fillSlot(namedSlot, element.outerHTML);
|
|
5521
|
+
}
|
|
5522
|
+
for (const slot of layout.ast.querySelectorAll("slot")) slot.replaceWith(slot.innerHTML.trim());
|
|
5523
|
+
}
|
|
5524
|
+
/**
|
|
5525
|
+
* Has a markdown page render inside the layout tag its front matter names: the naming key becomes the source attribute and the
|
|
5526
|
+
* other keys the tag's data.
|
|
5527
|
+
*/
|
|
5528
|
+
function wrapMarkdownInLayout(metadata, names) {
|
|
5529
|
+
const tag = Object.keys(metadata.ast.frontmatter).find((key) => names.tags.includes(key));
|
|
5530
|
+
if (!tag) return;
|
|
5531
|
+
let attributes = "";
|
|
5532
|
+
for (const [key, value] of Object.entries(metadata.ast.frontmatter)) attributes += `${key === tag ? names.source : key}="${value}" `;
|
|
5533
|
+
const openTag = `<${tag} ${attributes}>`;
|
|
5534
|
+
const closeTag = `</${tag}>`;
|
|
5535
|
+
const render = metadata.ast.render;
|
|
5536
|
+
metadata.ast.render = async () => {
|
|
5537
|
+
const html = await render();
|
|
5538
|
+
return openTag + html + closeTag;
|
|
5539
|
+
};
|
|
5540
|
+
}
|
|
5541
|
+
/** Whether a conditional attribute's value, after the placeholders are filled, switches the attribute on. */
|
|
5542
|
+
function isSwitchedOn(value) {
|
|
5543
|
+
const trimmed = value.trim();
|
|
5544
|
+
return trimmed !== "" && trimmed !== "false";
|
|
5545
|
+
}
|
|
5546
|
+
/** The tag's attributes other than the source, as the key a rendered instance is cached under. */
|
|
5547
|
+
function instanceKey(tag, names) {
|
|
5548
|
+
let key = "";
|
|
5549
|
+
for (const [attribute, value] of Object.entries(tag.attributes)) {
|
|
5550
|
+
if (attribute === names.source) continue;
|
|
5551
|
+
key += `${attribute}=${value}`;
|
|
5552
|
+
}
|
|
5553
|
+
return key;
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
//#endregion
|
|
5557
|
+
//#region src/plugins/html-layout/options.ts
|
|
5558
|
+
/** The options with their defaults filled in. */
|
|
5559
|
+
function resolveLayoutNames(options) {
|
|
5560
|
+
return {
|
|
5561
|
+
tags: options.tags ?? ["layout", "part"],
|
|
5562
|
+
source: options.layoutSourceAttribute ?? "src",
|
|
5563
|
+
dataSource: options.dataSourceAttribute ?? "dataSource",
|
|
5564
|
+
dataJson: options.dataJsonAttribute ?? "dataJson",
|
|
5565
|
+
alwaysInclude: options.alwaysIncludeAttribute ?? "always-include",
|
|
5566
|
+
addAttribute: options.addAttributeAttribute ?? "add-attr",
|
|
5567
|
+
removeAttribute: options.removeAttributeAttribute ?? "remove-attr"
|
|
5568
|
+
};
|
|
5569
|
+
}
|
|
5570
|
+
/** Whether a path is a file the tags inline: one named after a tag, `main.layout.html` say. */
|
|
5571
|
+
function isLayoutFile(file, names) {
|
|
5572
|
+
const fileName = basename(file);
|
|
5573
|
+
return names.tags.some((tag) => fileName.endsWith(`.${tag}.html`));
|
|
5574
|
+
}
|
|
5575
|
+
/** Whether an attribute adds or removes a boolean attribute. */
|
|
5576
|
+
function isConditionalAttribute(name, names) {
|
|
5577
|
+
return name.startsWith(`${names.addAttribute}-`) || name.startsWith(`${names.removeAttribute}-`);
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
//#endregion
|
|
5581
|
+
//#region src/plugins/html-layout/lsp.ts
|
|
5582
|
+
/** The tags and attributes the plugin adds to HTML, for completion and hover. */
|
|
5583
|
+
function htmlData$1(names) {
|
|
5584
|
+
const layoutAttributes = [
|
|
5585
|
+
{
|
|
5586
|
+
name: names.source,
|
|
5587
|
+
description: "The path to the layout to use."
|
|
5588
|
+
},
|
|
5589
|
+
{
|
|
5590
|
+
name: names.dataSource,
|
|
5591
|
+
description: "The path for a JSON file containing data for the layout."
|
|
5592
|
+
},
|
|
5593
|
+
{
|
|
5594
|
+
name: names.dataJson,
|
|
5595
|
+
description: "The layout data in JSON format."
|
|
5596
|
+
}
|
|
5597
|
+
];
|
|
5598
|
+
const alwaysInclude = {
|
|
5599
|
+
name: names.alwaysInclude,
|
|
5600
|
+
description: "When using a layout more than once on the same page. Always include this script/style/link[rel='stylesheet'] even if it is already present.",
|
|
5601
|
+
valueSet: "v"
|
|
5602
|
+
};
|
|
5603
|
+
const alwaysIncludeDescription = `Adds the \`${names.alwaysInclude}\` attribute, which gives this tag one copy per layout instance instead of the single copy a layout used more than once on the same page emits.`;
|
|
5604
|
+
const tags = [
|
|
5605
|
+
{
|
|
5606
|
+
name: "script",
|
|
5607
|
+
description: alwaysIncludeDescription,
|
|
5608
|
+
attributes: [alwaysInclude]
|
|
5609
|
+
},
|
|
5610
|
+
{
|
|
5611
|
+
name: "link",
|
|
5612
|
+
description: alwaysIncludeDescription,
|
|
5613
|
+
attributes: [alwaysInclude]
|
|
5614
|
+
},
|
|
5615
|
+
{
|
|
5616
|
+
name: "style",
|
|
5617
|
+
description: alwaysIncludeDescription,
|
|
5618
|
+
attributes: [alwaysInclude]
|
|
5619
|
+
}
|
|
5620
|
+
];
|
|
5621
|
+
for (const tag of names.tags) tags.push({
|
|
5622
|
+
name: tag,
|
|
5623
|
+
description: `This tag allows inlining a \`${tag}\` file.`,
|
|
5624
|
+
attributes: layoutAttributes
|
|
5625
|
+
});
|
|
5626
|
+
return {
|
|
5627
|
+
version: 1.1,
|
|
5628
|
+
tags,
|
|
5629
|
+
globalAttributes: [{
|
|
5630
|
+
name: names.addAttribute,
|
|
5631
|
+
description: "Inside a layout file only. Prefix for conditionally adding boolean attributes. Append the target attribute name as a suffix — `add-attr-open`, `add-attr-disabled`, etc. The value is a JS expression evaluated after placeholder resolution; if it is non-empty and not `\"false\"`, the named attribute is added to the element. Multiple `add-attr-*` attributes can appear on the same element."
|
|
5632
|
+
}, {
|
|
5633
|
+
name: names.removeAttribute,
|
|
5634
|
+
description: "Inside a layout file only. Prefix for conditionally removing boolean attributes. Append the target attribute name as a suffix — `remove-attr-open`, `remove-attr-disabled`, etc. The value is a JS expression evaluated after placeholder resolution; if it is non-empty and not `\"false\"`, the named attribute is removed from the element. Multiple `remove-attr-*` attributes can appear on the same element."
|
|
5635
|
+
}]
|
|
5636
|
+
};
|
|
5637
|
+
}
|
|
5638
|
+
/** The placeholders of layout files, as JavaScript that sees `$data`. */
|
|
5639
|
+
function embeddedLanguages$1(names) {
|
|
5640
|
+
return [{
|
|
5641
|
+
name: "placeholders",
|
|
5642
|
+
filter: (file) => isLayoutFile(file, names),
|
|
5643
|
+
findRegions: (document) => findPlaceholders(document.text),
|
|
5644
|
+
prelude: "declare const $data: Record<string, any>;"
|
|
5645
|
+
}];
|
|
5646
|
+
}
|
|
5647
|
+
/** Checks the layout tags of a document, and that the conditional attributes sit in a layout file. */
|
|
5648
|
+
function validate(names) {
|
|
5649
|
+
return (document, report) => {
|
|
5650
|
+
const layoutTags = document.select(...names.tags);
|
|
5651
|
+
for (const element of layoutTags) checkLayoutTag(element, document, report, names);
|
|
5652
|
+
if (isLayoutFile(document.file, names)) return;
|
|
5653
|
+
if (document.file.endsWith(".md")) return;
|
|
5654
|
+
for (const element of document.elements) for (const attribute of element.attributes) {
|
|
5655
|
+
if (!isConditionalAttribute(attribute.name, names)) continue;
|
|
5656
|
+
report.warn(attribute.nameRange, `"${attribute.name}" only works inside a layout file`);
|
|
5657
|
+
}
|
|
5658
|
+
};
|
|
5659
|
+
}
|
|
5660
|
+
/** A layout tag points at a file named after it, and its data attributes point at a JSON file and hold a JSON object. */
|
|
5661
|
+
function checkLayoutTag(element, document, report, names) {
|
|
5662
|
+
const source = element.attribute(names.source);
|
|
5663
|
+
const dataSource = element.attribute(names.dataSource);
|
|
5664
|
+
const dataJson = element.attribute(names.dataJson);
|
|
5665
|
+
if (!source?.value) report.error(element, `<${element.name}> needs a "${names.source}" pointing at a .${element.name}.html file`);
|
|
5666
|
+
if (source) {
|
|
5667
|
+
checkFileExists(source, document, report);
|
|
5668
|
+
checkFileSuffix(source, report);
|
|
5669
|
+
}
|
|
5670
|
+
if (dataSource) checkFileExists(dataSource, document, report);
|
|
5671
|
+
if (dataJson) checkJsonObject(dataJson, report);
|
|
5672
|
+
}
|
|
5673
|
+
/** A tag inlines files named after it, so `<part>` takes a `.part.html` file. */
|
|
5674
|
+
function checkFileSuffix(source, report) {
|
|
5675
|
+
if (!source.value) return;
|
|
5676
|
+
if (isDynamic(source.value)) return;
|
|
5677
|
+
const suffix = `.${source.element.name}.html`;
|
|
5678
|
+
if (source.value.endsWith(suffix)) return;
|
|
5679
|
+
report.warn(source, `<${source.element.name}> takes a file ending in "${suffix}"`);
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5235
5682
|
//#endregion
|
|
5236
5683
|
//#region src/plugins/html-layout/html-layout-plugin.ts
|
|
5237
5684
|
/**
|
|
@@ -5260,13 +5707,12 @@ function executeExpression$1(expressionString, data) {
|
|
|
5260
5707
|
* - In production an instance is rendered once per file and attribute set, then cloned for the identical ones.
|
|
5261
5708
|
*/
|
|
5262
5709
|
function htmlLayoutPlugin(options = {}) {
|
|
5263
|
-
const
|
|
5264
|
-
const
|
|
5265
|
-
const
|
|
5266
|
-
const
|
|
5267
|
-
const
|
|
5268
|
-
const
|
|
5269
|
-
const removeAttributeAttribute = options.removeAttributeAttribute ?? "remove-attr";
|
|
5710
|
+
const names = resolveLayoutNames(options);
|
|
5711
|
+
const layoutTags = names.tags;
|
|
5712
|
+
const sourceAttribute = names.source;
|
|
5713
|
+
const dataSourceAttribute = names.dataSource;
|
|
5714
|
+
const dataJsonAttribute = names.dataJson;
|
|
5715
|
+
const alwaysIncludeAttribute = names.alwaysInclude;
|
|
5270
5716
|
const query = layoutTags.join(",");
|
|
5271
5717
|
const printFmtError = PrintFormattedError.create({ function: htmlLayoutPlugin });
|
|
5272
5718
|
const htmlLayoutMetadataCache = [];
|
|
@@ -5276,8 +5722,7 @@ function htmlLayoutPlugin(options = {}) {
|
|
|
5276
5722
|
*/
|
|
5277
5723
|
const dataSourceCache = /* @__PURE__ */ new Map();
|
|
5278
5724
|
function isLayoutPath(path) {
|
|
5279
|
-
|
|
5280
|
-
return layoutTags.some((tag) => fileName.endsWith(`.${tag}.html`));
|
|
5725
|
+
return isLayoutFile(path, names);
|
|
5281
5726
|
}
|
|
5282
5727
|
return {
|
|
5283
5728
|
name: "html-layout",
|
|
@@ -5432,7 +5877,7 @@ function htmlLayoutPlugin(options = {}) {
|
|
|
5432
5877
|
continue;
|
|
5433
5878
|
}
|
|
5434
5879
|
let layoutHtmlMetadata;
|
|
5435
|
-
const hash =
|
|
5880
|
+
const hash = instanceKey(node, names);
|
|
5436
5881
|
if (this.production) {
|
|
5437
5882
|
const cachedMetadata = htmlLayoutMetadataCache.find((metadata) => metadata.id === layoutRelativePath && metadata.hash === hash);
|
|
5438
5883
|
if (cachedMetadata) layoutHtmlMetadata = this.clone(cachedMetadata);
|
|
@@ -5453,25 +5898,7 @@ function htmlLayoutPlugin(options = {}) {
|
|
|
5453
5898
|
const metadataList = await this.resolveAndLoad(layoutMetadata);
|
|
5454
5899
|
this.addMetadata(metadataList.slice(1));
|
|
5455
5900
|
await this.transformAndSync(metadataList);
|
|
5456
|
-
|
|
5457
|
-
const removeAttributePrefix = `${removeAttributeAttribute}-`;
|
|
5458
|
-
const elements = layoutMetadata.ast.querySelectorAll("*");
|
|
5459
|
-
for (const element of elements) {
|
|
5460
|
-
if (!element.attributes) continue;
|
|
5461
|
-
for (const [name, value] of Object.entries(element.attributes)) {
|
|
5462
|
-
if (name.startsWith(addAttributePrefix)) {
|
|
5463
|
-
const targetAttribute = name.slice(addAttributePrefix.length);
|
|
5464
|
-
element.removeAttribute(name);
|
|
5465
|
-
if (value.trim() && value.trim() !== "false") element.setAttribute(targetAttribute, "");
|
|
5466
|
-
continue;
|
|
5467
|
-
}
|
|
5468
|
-
if (name.startsWith(removeAttributePrefix)) {
|
|
5469
|
-
const targetAttribute = name.slice(removeAttributePrefix.length);
|
|
5470
|
-
element.removeAttribute(name);
|
|
5471
|
-
if (value.trim() && value.trim() !== "false") element.removeAttribute(targetAttribute);
|
|
5472
|
-
}
|
|
5473
|
-
}
|
|
5474
|
-
}
|
|
5901
|
+
applyConditionalAttributes(layoutMetadata, names);
|
|
5475
5902
|
if (this.production) {
|
|
5476
5903
|
layoutMetadata.hash = hash;
|
|
5477
5904
|
htmlLayoutMetadataCache.push(layoutMetadata);
|
|
@@ -5486,67 +5913,10 @@ function htmlLayoutPlugin(options = {}) {
|
|
|
5486
5913
|
skip();
|
|
5487
5914
|
continue;
|
|
5488
5915
|
}
|
|
5489
|
-
|
|
5490
|
-
for (const scriptOrStyle of scriptsAndStyles) {
|
|
5491
|
-
const id = scriptOrStyle.getAttribute(CUSTOM_ATTRIBUTES.MetadataID);
|
|
5492
|
-
if (!id) continue;
|
|
5493
|
-
if (scriptOrStyle.hasAttribute(alwaysIncludeAttribute)) {
|
|
5494
|
-
scriptOrStyle.removeAttribute(alwaysIncludeAttribute);
|
|
5495
|
-
continue;
|
|
5496
|
-
}
|
|
5497
|
-
const isScript = scriptOrStyle.tagName === "SCRIPT";
|
|
5498
|
-
if (metadata[isScript ? "scriptsMetadataList" : "stylesMetadataList"].get(id)) {
|
|
5499
|
-
layoutHtmlMetadata[isScript ? "scriptsMetadataList" : "stylesMetadataList"].delete(id);
|
|
5500
|
-
scriptOrStyle.remove();
|
|
5501
|
-
}
|
|
5502
|
-
}
|
|
5503
|
-
const scriptsAndLinks = layoutHtmlMetadata.ast.querySelectorAll("script[src], link[rel=\"stylesheet\"]");
|
|
5504
|
-
for (const scriptOrLink of scriptsAndLinks) {
|
|
5505
|
-
if (scriptOrLink.hasAttribute(alwaysIncludeAttribute)) {
|
|
5506
|
-
scriptOrLink.removeAttribute(alwaysIncludeAttribute);
|
|
5507
|
-
continue;
|
|
5508
|
-
}
|
|
5509
|
-
const isScript = scriptOrLink.tagName === "SCRIPT";
|
|
5510
|
-
const attribute = isScript ? "src" : "href";
|
|
5511
|
-
const layoutSourceRelative = join(dirname(layoutHtmlMetadata.filePath), scriptOrLink.attributes[attribute]);
|
|
5512
|
-
const selector = isScript ? "script[src]" : "link[rel=\"stylesheet\"]";
|
|
5513
|
-
if (metadata.ast.querySelectorAll(selector).some((element) => {
|
|
5514
|
-
return join(dirname(metadata.filePath), element.attributes[attribute]) === layoutSourceRelative;
|
|
5515
|
-
})) scriptOrLink.remove();
|
|
5516
|
-
}
|
|
5916
|
+
dropRepeatedAssets(metadata, layoutHtmlMetadata, alwaysIncludeAttribute);
|
|
5517
5917
|
for (const dependency of layoutHtmlMetadata.directDependencies) metadata.directDependencies.add(dependency);
|
|
5518
5918
|
await this.rebase(layoutHtmlMetadata, join(this.root, metadata.filePath));
|
|
5519
|
-
|
|
5520
|
-
const fillSlot = (slot, html) => {
|
|
5521
|
-
if (!filledSlots.has(slot)) {
|
|
5522
|
-
filledSlots.add(slot);
|
|
5523
|
-
slot.innerHTML = "";
|
|
5524
|
-
}
|
|
5525
|
-
slot.insertAdjacentHTML("beforeend", html);
|
|
5526
|
-
};
|
|
5527
|
-
const children = node.querySelectorAll(":scope > *");
|
|
5528
|
-
for (const element of children) {
|
|
5529
|
-
if (element.nodeType !== NodeType.ELEMENT_NODE) continue;
|
|
5530
|
-
if (!element.hasAttribute("slot")) {
|
|
5531
|
-
const defaultSlots = layoutHtmlMetadata.ast.querySelectorAll(`slot:not([name])`);
|
|
5532
|
-
if (defaultSlots.length === 0) {
|
|
5533
|
-
printFmtError(`No default slot in layout file:`, layoutRelativePath, { node: element });
|
|
5534
|
-
continue;
|
|
5535
|
-
}
|
|
5536
|
-
for (const defaultSlot of defaultSlots) fillSlot(defaultSlot, element.outerHTML);
|
|
5537
|
-
continue;
|
|
5538
|
-
}
|
|
5539
|
-
const slotName = element.getAttribute("slot");
|
|
5540
|
-
if (!slotName) continue;
|
|
5541
|
-
const namedSlots = layoutHtmlMetadata.ast.querySelectorAll(`slot[name="${slotName}"]`);
|
|
5542
|
-
if (namedSlots.length === 0) {
|
|
5543
|
-
printFmtError(`No slots with name "${slotName}" in layout file:`, layoutRelativePath, { node: element });
|
|
5544
|
-
continue;
|
|
5545
|
-
}
|
|
5546
|
-
element.removeAttribute("slot");
|
|
5547
|
-
for (const namedSlot of namedSlots) fillSlot(namedSlot, element.outerHTML);
|
|
5548
|
-
}
|
|
5549
|
-
for (const slot of layoutHtmlMetadata.ast.querySelectorAll("slot")) slot.replaceWith(slot.innerHTML.trim());
|
|
5919
|
+
fillSlots(node, layoutHtmlMetadata, layoutRelativePath, printFmtError);
|
|
5550
5920
|
mergeMaps(metadata.scriptsMetadataList, layoutHtmlMetadata.scriptsMetadataList);
|
|
5551
5921
|
mergeMaps(metadata.stylesMetadataList, layoutHtmlMetadata.stylesMetadataList);
|
|
5552
5922
|
node.replaceWith(layoutHtmlMetadata.ast.root);
|
|
@@ -5554,86 +5924,17 @@ function htmlLayoutPlugin(options = {}) {
|
|
|
5554
5924
|
}
|
|
5555
5925
|
return;
|
|
5556
5926
|
}
|
|
5557
|
-
if (isMarkdownMetadata(metadata))
|
|
5558
|
-
const tag = Object.keys(metadata.ast.frontmatter).find((key) => layoutTags.includes(key));
|
|
5559
|
-
if (!tag) return;
|
|
5560
|
-
let attributesString = "";
|
|
5561
|
-
for (const [key, value] of Object.entries(metadata.ast.frontmatter)) attributesString += `${key === tag ? sourceAttribute : key}="${value}" `;
|
|
5562
|
-
const openTag = `<${tag} ${attributesString}>`;
|
|
5563
|
-
const closeTag = `</${tag}>`;
|
|
5564
|
-
const oldRender = metadata.ast.render;
|
|
5565
|
-
metadata.ast.render = async () => {
|
|
5566
|
-
const html = await oldRender();
|
|
5567
|
-
return openTag + html + closeTag;
|
|
5568
|
-
};
|
|
5569
|
-
return;
|
|
5570
|
-
}
|
|
5927
|
+
if (isMarkdownMetadata(metadata)) wrapMarkdownInLayout(metadata, names);
|
|
5571
5928
|
},
|
|
5572
5929
|
lspHtmlData() {
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
name: dataSourceAttribute,
|
|
5580
|
-
description: "The path for a JSON file containing data for the layout."
|
|
5581
|
-
},
|
|
5582
|
-
{
|
|
5583
|
-
name: dataJsonAttribute,
|
|
5584
|
-
description: "The layout data in JSON format."
|
|
5585
|
-
}
|
|
5586
|
-
];
|
|
5587
|
-
const alwaysInclude = {
|
|
5588
|
-
name: alwaysIncludeAttribute,
|
|
5589
|
-
description: "When using a layout more than once on the same page. Always include this script/style/link[rel='stylesheet'] even if it is already present.",
|
|
5590
|
-
valueSet: "v"
|
|
5591
|
-
};
|
|
5592
|
-
const alwaysIncludeDescription = `Adds the \`${alwaysIncludeAttribute}\` attribute, which gives this tag one copy per layout instance instead of the single copy a layout used more than once on the same page emits.`;
|
|
5593
|
-
const tags = [
|
|
5594
|
-
{
|
|
5595
|
-
name: "script",
|
|
5596
|
-
description: alwaysIncludeDescription,
|
|
5597
|
-
attributes: [alwaysInclude]
|
|
5598
|
-
},
|
|
5599
|
-
{
|
|
5600
|
-
name: "link",
|
|
5601
|
-
description: alwaysIncludeDescription,
|
|
5602
|
-
attributes: [alwaysInclude]
|
|
5603
|
-
},
|
|
5604
|
-
{
|
|
5605
|
-
name: "style",
|
|
5606
|
-
description: alwaysIncludeDescription,
|
|
5607
|
-
attributes: [alwaysInclude]
|
|
5608
|
-
}
|
|
5609
|
-
];
|
|
5610
|
-
for (const tag of layoutTags) tags.push({
|
|
5611
|
-
name: tag,
|
|
5612
|
-
description: `This tag allows inlining a \`${tag}\` file.`,
|
|
5613
|
-
attributes
|
|
5614
|
-
});
|
|
5615
|
-
return {
|
|
5616
|
-
version: 1.1,
|
|
5617
|
-
tags,
|
|
5618
|
-
globalAttributes: [{
|
|
5619
|
-
name: addAttributeAttribute,
|
|
5620
|
-
description: "Inside a layout file only. Prefix for conditionally adding boolean attributes. Append the target attribute name as a suffix — `add-attr-open`, `add-attr-disabled`, etc. The value is a JS expression evaluated after placeholder resolution; if it is non-empty and not `\"false\"`, the named attribute is added to the element. Multiple `add-attr-*` attributes can appear on the same element."
|
|
5621
|
-
}, {
|
|
5622
|
-
name: removeAttributeAttribute,
|
|
5623
|
-
description: "Inside a layout file only. Prefix for conditionally removing boolean attributes. Append the target attribute name as a suffix — `remove-attr-open`, `remove-attr-disabled`, etc. The value is a JS expression evaluated after placeholder resolution; if it is non-empty and not `\"false\"`, the named attribute is removed from the element. Multiple `remove-attr-*` attributes can appear on the same element."
|
|
5624
|
-
}]
|
|
5625
|
-
};
|
|
5626
|
-
}
|
|
5930
|
+
return htmlData$1(names);
|
|
5931
|
+
},
|
|
5932
|
+
lspEmbeddedLanguages() {
|
|
5933
|
+
return embeddedLanguages$1(names);
|
|
5934
|
+
},
|
|
5935
|
+
lspValidate: validate(names)
|
|
5627
5936
|
};
|
|
5628
5937
|
}
|
|
5629
|
-
function calculateHash(node, sourceAttribute) {
|
|
5630
|
-
let hash = "";
|
|
5631
|
-
for (const [attribute, value] of Object.entries(node.attributes)) {
|
|
5632
|
-
if (attribute === sourceAttribute) continue;
|
|
5633
|
-
hash += `${attribute}=${value}`;
|
|
5634
|
-
}
|
|
5635
|
-
return hash;
|
|
5636
|
-
}
|
|
5637
5938
|
function isHtmlLayoutMetadata(metadata) {
|
|
5638
5939
|
return isHtmlMetadata(metadata);
|
|
5639
5940
|
}
|
|
@@ -5789,6 +6090,23 @@ function htmlMarkdownPlugin(options = {}) {
|
|
|
5789
6090
|
}]
|
|
5790
6091
|
}]
|
|
5791
6092
|
};
|
|
6093
|
+
},
|
|
6094
|
+
lspValidate(document, report) {
|
|
6095
|
+
const markdowns = document.select(markdownTag);
|
|
6096
|
+
for (const element of markdowns) {
|
|
6097
|
+
const source = element.attribute(sourceAttribute);
|
|
6098
|
+
if (!source?.value) {
|
|
6099
|
+
report.error(element, `<${markdownTag}> needs a "${sourceAttribute}" pointing at a markdown file`);
|
|
6100
|
+
continue;
|
|
6101
|
+
}
|
|
6102
|
+
checkFileExists(source, document, report);
|
|
6103
|
+
}
|
|
6104
|
+
for (const element of document.elements) {
|
|
6105
|
+
const markdown = element.attribute(mdAttribute);
|
|
6106
|
+
if (!markdown) continue;
|
|
6107
|
+
if (!isEmptyElement(element, document)) continue;
|
|
6108
|
+
report.warn(markdown.nameRange, `There is no markdown in this <${element.name}> to render`);
|
|
6109
|
+
}
|
|
5792
6110
|
}
|
|
5793
6111
|
};
|
|
5794
6112
|
}
|
|
@@ -6569,9 +6887,30 @@ You can narrow down the files to preload by specifying a space-separated list of
|
|
|
6569
6887
|
attributes
|
|
6570
6888
|
}]
|
|
6571
6889
|
};
|
|
6890
|
+
},
|
|
6891
|
+
lspValidate(document, report) {
|
|
6892
|
+
for (const element of document.elements) {
|
|
6893
|
+
const preload = element.attribute(preloadAttribute);
|
|
6894
|
+
if (!preload) continue;
|
|
6895
|
+
if (!isPreloadable(element)) {
|
|
6896
|
+
report.warn(preload.nameRange, `"${preloadAttribute}" only works on a script, a style or a stylesheet link`);
|
|
6897
|
+
continue;
|
|
6898
|
+
}
|
|
6899
|
+
if (!preload.value) continue;
|
|
6900
|
+
if (isDynamic(preload.value)) continue;
|
|
6901
|
+
const unknownTypes = preload.value.split(" ").filter((type) => type && !asTypes.includes(type));
|
|
6902
|
+
if (unknownTypes.length === 0) continue;
|
|
6903
|
+
report.error(preload, `Unknown preload types ${unknownTypes.join(", ")}, use ${asTypes.join(", ")}`);
|
|
6904
|
+
}
|
|
6572
6905
|
}
|
|
6573
6906
|
};
|
|
6574
6907
|
}
|
|
6908
|
+
/** Whether the preload attribute means something on an element: a JavaScript script, a style or a stylesheet link. */
|
|
6909
|
+
function isPreloadable(element) {
|
|
6910
|
+
if (element.name === "script") return isJavaScript(element);
|
|
6911
|
+
if (element.name === "style") return true;
|
|
6912
|
+
return element.name === "link" && element.attribute("rel")?.value === "stylesheet";
|
|
6913
|
+
}
|
|
6575
6914
|
function getMetadata(metadataList, htmlMetadata, node) {
|
|
6576
6915
|
const isScriptTag = node.tagName === "SCRIPT";
|
|
6577
6916
|
const isStyleTag = node.tagName === "STYLE";
|
|
@@ -6734,6 +7073,13 @@ async function htmlLoader(relativePath, code) {
|
|
|
6734
7073
|
const scriptsMetadataList = /* @__PURE__ */ new Map();
|
|
6735
7074
|
const scripts = rootElement.querySelectorAll("script");
|
|
6736
7075
|
for (const script of scripts) {
|
|
7076
|
+
if (script.getAttribute("type")?.toLowerCase() === "application/x-typescript") {
|
|
7077
|
+
script.removeAttribute("type");
|
|
7078
|
+
if (script.hasAttribute("module")) {
|
|
7079
|
+
script.removeAttribute("module");
|
|
7080
|
+
script.setAttribute("type", "module");
|
|
7081
|
+
}
|
|
7082
|
+
}
|
|
6737
7083
|
if (script.hasAttribute("src")) continue;
|
|
6738
7084
|
const scriptType = script.getAttribute("type");
|
|
6739
7085
|
if (!isScriptType(scriptType)) continue;
|
|
@@ -6774,6 +7120,46 @@ async function htmlLoader(relativePath, code) {
|
|
|
6774
7120
|
}, null];
|
|
6775
7121
|
}
|
|
6776
7122
|
|
|
7123
|
+
//#endregion
|
|
7124
|
+
//#region src/plugins/core-plugins/html-metadata/lsp.ts
|
|
7125
|
+
/**
|
|
7126
|
+
* The bodies of the inline scripts marked `type="${TYPESCRIPT_TYPE}"`, served as TypeScript through the project's own TypeScript,
|
|
7127
|
+
* so they see the browser the way the project's tsconfig describes it. A plugin that runs a script elsewhere claims it over
|
|
7128
|
+
* this.
|
|
7129
|
+
*/
|
|
7130
|
+
function embeddedLanguages() {
|
|
7131
|
+
return [{
|
|
7132
|
+
name: "scripts",
|
|
7133
|
+
filter: () => true,
|
|
7134
|
+
findRegions: (document) => document.select("script").flatMap((script) => {
|
|
7135
|
+
if (!isTypeScriptScript(script) || script.has("src") || !script.contentRange) return [];
|
|
7136
|
+
return {
|
|
7137
|
+
...script.contentRange,
|
|
7138
|
+
isModule: script.has("module")
|
|
7139
|
+
};
|
|
7140
|
+
}),
|
|
7141
|
+
isColouredByEditor: true
|
|
7142
|
+
}];
|
|
7143
|
+
}
|
|
7144
|
+
/** The attributes the plugin adds to `<script>`, for completion and hover. */
|
|
7145
|
+
function htmlData() {
|
|
7146
|
+
return {
|
|
7147
|
+
version: 1.1,
|
|
7148
|
+
tags: [{
|
|
7149
|
+
name: "script",
|
|
7150
|
+
attributes: [{
|
|
7151
|
+
name: "type",
|
|
7152
|
+
description: `\`${TYPESCRIPT_TYPE}\` marks the script as TypeScript for the editor: Prettier formats it as such, the staticbolt language server serves it through the project's TypeScript, and the build drops the type.`,
|
|
7153
|
+
values: [{ name: TYPESCRIPT_TYPE }]
|
|
7154
|
+
}, {
|
|
7155
|
+
name: "module",
|
|
7156
|
+
description: `With \`type="${TYPESCRIPT_TYPE}"\`, whose type slot is taken: the build prints the script as \`type="module"\`.`,
|
|
7157
|
+
valueSet: "v"
|
|
7158
|
+
}]
|
|
7159
|
+
}]
|
|
7160
|
+
};
|
|
7161
|
+
}
|
|
7162
|
+
|
|
6777
7163
|
//#endregion
|
|
6778
7164
|
//#region src/plugins/core-plugins/html-metadata/html-metadata-plugin.ts
|
|
6779
7165
|
/**
|
|
@@ -6898,6 +7284,12 @@ function coreHtmlPlugin(options = {}) {
|
|
|
6898
7284
|
metadata.filePath = relative(this.root, newAbsolutePath);
|
|
6899
7285
|
for (const [, scriptMetadata] of metadata.scriptsMetadataList) scriptMetadata.filePath = metadata.filePath;
|
|
6900
7286
|
for (const [, cssMetadata] of metadata.stylesMetadataList) cssMetadata.filePath = metadata.filePath;
|
|
7287
|
+
},
|
|
7288
|
+
lspEmbeddedLanguages() {
|
|
7289
|
+
return embeddedLanguages();
|
|
7290
|
+
},
|
|
7291
|
+
lspHtmlData() {
|
|
7292
|
+
return htmlData();
|
|
6901
7293
|
}
|
|
6902
7294
|
};
|
|
6903
7295
|
}
|
|
@@ -7568,6 +7960,104 @@ function coreWebManifestPlugin() {
|
|
|
7568
7960
|
};
|
|
7569
7961
|
}
|
|
7570
7962
|
|
|
7963
|
+
//#endregion
|
|
7964
|
+
//#region src/plugins/core-plugins/development-server/claim-port.ts
|
|
7965
|
+
const PROJECT_HEADER = "x-staticbolt-project";
|
|
7966
|
+
const PID_HEADER = "x-staticbolt-pid";
|
|
7967
|
+
const FREE_PORT_SEARCH = 20;
|
|
7968
|
+
const KILL_TIMEOUT_MS = 3e3;
|
|
7969
|
+
/**
|
|
7970
|
+
* The port to listen on: `port` when it is free, otherwise what the user picks. Another dev server of ours can be killed to take
|
|
7971
|
+
* its port over, any holder can be sidestepped with the next free port, and declining, or having no terminal to ask on, fails
|
|
7972
|
+
* with the wanted port's error.
|
|
7973
|
+
*/
|
|
7974
|
+
async function claimPort(port, host, project) {
|
|
7975
|
+
if (await isPortFree(port, host)) return port;
|
|
7976
|
+
const owner = await identifyOwner(port);
|
|
7977
|
+
const problem = describeOwner(owner, port, project);
|
|
7978
|
+
const choice = process.stdin.isTTY ? await ask(problem, await listChoices(owner, port, host)) : void 0;
|
|
7979
|
+
if (choice?.port === void 0) throw new Error(`${problem} Stop what is using it, or pick another with developmentServerPlugin({ port })`, { cause: owner });
|
|
7980
|
+
if (choice.killPid !== void 0) await kill(choice.killPid, port, host);
|
|
7981
|
+
return choice.port;
|
|
7982
|
+
}
|
|
7983
|
+
/** A `HEAD /` answered with the project header means a dev server of ours. Anything else, or no answer, is another application. */
|
|
7984
|
+
async function identifyOwner(port) {
|
|
7985
|
+
try {
|
|
7986
|
+
const response = await fetch(`http://127.0.0.1:${port}/`, {
|
|
7987
|
+
method: "HEAD",
|
|
7988
|
+
signal: AbortSignal.timeout(1e3)
|
|
7989
|
+
});
|
|
7990
|
+
const project = response.headers.get(PROJECT_HEADER);
|
|
7991
|
+
const pid = Number(response.headers.get(PID_HEADER));
|
|
7992
|
+
return project && pid ? {
|
|
7993
|
+
kind: "staticbolt",
|
|
7994
|
+
project,
|
|
7995
|
+
pid
|
|
7996
|
+
} : { kind: "other" };
|
|
7997
|
+
} catch {
|
|
7998
|
+
return { kind: "other" };
|
|
7999
|
+
}
|
|
8000
|
+
}
|
|
8001
|
+
function describeOwner(owner, port, project) {
|
|
8002
|
+
if (owner.kind === "other") return `Port ${port} is used by another application.`;
|
|
8003
|
+
if (owner.project === project) return `A dev server for this project is already running on port ${port}.`;
|
|
8004
|
+
return `Port ${port} is used by the dev server of "${owner.project}".`;
|
|
8005
|
+
}
|
|
8006
|
+
async function listChoices(owner, port, host) {
|
|
8007
|
+
const choices = [];
|
|
8008
|
+
if (owner.kind === "staticbolt") choices.push({
|
|
8009
|
+
label: `Kill it and use port ${port}`,
|
|
8010
|
+
port,
|
|
8011
|
+
killPid: owner.pid
|
|
8012
|
+
});
|
|
8013
|
+
const freePort = await findFreePort(port + 1, host);
|
|
8014
|
+
if (freePort !== void 0) choices.push({
|
|
8015
|
+
label: `Use port ${freePort}`,
|
|
8016
|
+
port: freePort
|
|
8017
|
+
});
|
|
8018
|
+
choices.push({ label: "Do nothing" });
|
|
8019
|
+
return choices;
|
|
8020
|
+
}
|
|
8021
|
+
async function kill(pid, port, host) {
|
|
8022
|
+
process.kill(pid);
|
|
8023
|
+
const deadline = Date.now() + KILL_TIMEOUT_MS;
|
|
8024
|
+
while (Date.now() < deadline) {
|
|
8025
|
+
if (await isPortFree(port, host)) return;
|
|
8026
|
+
await setTimeout$1(100);
|
|
8027
|
+
}
|
|
8028
|
+
throw new Error(`Port ${port} is still busy after stopping process ${pid}`);
|
|
8029
|
+
}
|
|
8030
|
+
async function findFreePort(from, host) {
|
|
8031
|
+
for (let port = from; port < from + FREE_PORT_SEARCH; port++) if (await isPortFree(port, host)) return port;
|
|
8032
|
+
}
|
|
8033
|
+
function isPortFree(port, host) {
|
|
8034
|
+
return new Promise((resolve) => {
|
|
8035
|
+
const server = createServer();
|
|
8036
|
+
server.once("error", () => resolve(false));
|
|
8037
|
+
server.listen(port, host, () => server.close(() => resolve(true)));
|
|
8038
|
+
});
|
|
8039
|
+
}
|
|
8040
|
+
/** Numbered choices in the terminal. An empty answer takes the first, a closed input the last. */
|
|
8041
|
+
async function ask(question, choices) {
|
|
8042
|
+
const readline = createInterface({
|
|
8043
|
+
input: process.stdin,
|
|
8044
|
+
output: process.stdout
|
|
8045
|
+
});
|
|
8046
|
+
try {
|
|
8047
|
+
Log.warn(question);
|
|
8048
|
+
for (const [index, choice] of choices.entries()) console.log(` ${chalk.bold(index + 1)}) ${choice.label}`);
|
|
8049
|
+
while (true) {
|
|
8050
|
+
const answer = await readline.question(chalk.bold(" Choice (1): "));
|
|
8051
|
+
const picked = choices[answer.trim() === "" ? 0 : Number(answer) - 1];
|
|
8052
|
+
if (picked) return picked;
|
|
8053
|
+
}
|
|
8054
|
+
} catch {
|
|
8055
|
+
return choices.at(-1);
|
|
8056
|
+
} finally {
|
|
8057
|
+
readline.close();
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
|
|
7571
8061
|
//#endregion
|
|
7572
8062
|
//#region src/plugins/core-plugins/development-server/inject-reload-script.ts
|
|
7573
8063
|
const id = "inject-reload-script";
|
|
@@ -7725,6 +8215,7 @@ function orderByDependencyDepth(allMetadata, entryPoints, filePaths) {
|
|
|
7725
8215
|
|
|
7726
8216
|
//#endregion
|
|
7727
8217
|
//#region src/plugins/core-plugins/development-server/development-server-plugin.ts
|
|
8218
|
+
const DEVELOPMENT_SERVER_OVERRIDES = "development-server";
|
|
7728
8219
|
/**
|
|
7729
8220
|
* The dev server: serves the build from memory over Fastify, compiles pages on demand, and pushes updates to the browser over a
|
|
7730
8221
|
* web socket.
|
|
@@ -7743,7 +8234,6 @@ function orderByDependencyDepth(allMetadata, entryPoints, filePaths) {
|
|
|
7743
8234
|
* itself as soon as it reconnects.
|
|
7744
8235
|
*/
|
|
7745
8236
|
function developmentServerPlugin(options = {}) {
|
|
7746
|
-
const host = options.host ?? "0.0.0.0";
|
|
7747
8237
|
const port = options.port ?? 3e3;
|
|
7748
8238
|
const entry = options.entry ?? "/";
|
|
7749
8239
|
const publicDirectory = options.publicDirectory ?? "public";
|
|
@@ -7773,7 +8263,9 @@ function developmentServerPlugin(options = {}) {
|
|
|
7773
8263
|
if (this.production) return;
|
|
7774
8264
|
const websocketServer = new WebSocketServer({ noServer: true });
|
|
7775
8265
|
websocket = websocketServer;
|
|
8266
|
+
const projectName = basename(this.root);
|
|
7776
8267
|
const fastifyServe = async (request, reply) => {
|
|
8268
|
+
reply.header(PROJECT_HEADER, projectName).header(PID_HEADER, process.pid);
|
|
7777
8269
|
const requestPath = normalize("." + decodeURIComponent(request.urlData().path || "./"));
|
|
7778
8270
|
await this.handleRequest(requestPath, reply, request);
|
|
7779
8271
|
};
|
|
@@ -7790,11 +8282,14 @@ function developmentServerPlugin(options = {}) {
|
|
|
7790
8282
|
removeAndPruneOrphans(this.metadataList, id);
|
|
7791
8283
|
}
|
|
7792
8284
|
});
|
|
7793
|
-
const
|
|
7794
|
-
|
|
8285
|
+
const overrides = this.pluginData[DEVELOPMENT_SERVER_OVERRIDES];
|
|
8286
|
+
const host = resolveHost(overrides?.host ?? options.host);
|
|
8287
|
+
const listeningPort = await claimPort(port, host, projectName);
|
|
8288
|
+
await fastify.listen({
|
|
8289
|
+
port: listeningPort,
|
|
7795
8290
|
host
|
|
7796
8291
|
});
|
|
7797
|
-
this.log.info(
|
|
8292
|
+
this.log.info(describeAddresses(host, listeningPort, entry));
|
|
7798
8293
|
},
|
|
7799
8294
|
async teardown() {
|
|
7800
8295
|
for (const client of wsClients) client.terminate();
|
|
@@ -7867,6 +8362,19 @@ function developmentServerPlugin(options = {}) {
|
|
|
7867
8362
|
}
|
|
7868
8363
|
};
|
|
7869
8364
|
}
|
|
8365
|
+
/** The URLs the server answers on: localhost, plus every LAN address when it is bound to all interfaces. */
|
|
8366
|
+
function describeAddresses(host, port, entry) {
|
|
8367
|
+
const isEveryInterface = host === "0.0.0.0" || host === "::";
|
|
8368
|
+
const isLoopback = host === "127.0.0.1" || host === "::1";
|
|
8369
|
+
const url = (hostname) => chalk.green(`http://${hostname}:${port}${entry}`);
|
|
8370
|
+
const lines = [`${chalk.bold("Local:")} ${url(isEveryInterface || isLoopback ? "localhost" : host)}`];
|
|
8371
|
+
if (isEveryInterface) for (const address of lanAddresses()) lines.push(`${chalk.bold("Network:")} ${url(address)}`);
|
|
8372
|
+
else if (isLoopback) lines.push(`${chalk.bold("Network:")} ${chalk.dim("use --host to expose")}`);
|
|
8373
|
+
return "\n" + lines.join("\n");
|
|
8374
|
+
}
|
|
8375
|
+
function lanAddresses() {
|
|
8376
|
+
return Object.values(networkInterfaces()).flat().filter((address) => address?.family === "IPv4" && !address.internal).map((address) => address.address);
|
|
8377
|
+
}
|
|
7870
8378
|
function isDependency(allMetadata, metadata, filePath) {
|
|
7871
8379
|
if (metadata.directDependencies.has(filePath)) return true;
|
|
7872
8380
|
const metadataByPath = new Map(allMetadata.map((m) => [m.id, m]));
|
|
@@ -7909,6 +8417,12 @@ function removeAndPruneOrphans(metadataList, targetId) {
|
|
|
7909
8417
|
for (const dependencyId of dependencies) if (!metadataList.some((m) => m.directDependencies.has(dependencyId))) queue.push(dependencyId);
|
|
7910
8418
|
}
|
|
7911
8419
|
}
|
|
8420
|
+
/** Every address, a given one, or the IPv4 loopback: "localhost" itself can resolve to IPv6 only, Windows being the usual case. */
|
|
8421
|
+
function resolveHost(host) {
|
|
8422
|
+
if (host === true) return "0.0.0.0";
|
|
8423
|
+
if (typeof host === "string" && host !== "") return host;
|
|
8424
|
+
return "127.0.0.1";
|
|
8425
|
+
}
|
|
7912
8426
|
|
|
7913
8427
|
//#endregion
|
|
7914
8428
|
//#region src/helpers/pirnt-debug.ts
|
|
@@ -10205,9 +10719,14 @@ function serveCliPlugin(options = {}) {
|
|
|
10205
10719
|
const serveCommand = this.defineSubcommand({
|
|
10206
10720
|
name,
|
|
10207
10721
|
aliases,
|
|
10208
|
-
meta: { description: "Dev server for staticbolt." }
|
|
10722
|
+
meta: { description: "Dev server for staticbolt." },
|
|
10723
|
+
options: { host: {
|
|
10724
|
+
schema: z.boolean().optional(),
|
|
10725
|
+
coerce: this.coerce.boolean,
|
|
10726
|
+
meta: { description: "Listen on every address, so the site is reachable from other devices on the network." }
|
|
10727
|
+
} }
|
|
10209
10728
|
});
|
|
10210
|
-
serveCommand.onExecute(() => new DevelopmentRun(config, configPath, projectDirectory).start());
|
|
10729
|
+
serveCommand.onExecute(({ options }) => new DevelopmentRun(config, configPath, projectDirectory, { host: options.host }).start());
|
|
10211
10730
|
this.addCommand(serveCommand);
|
|
10212
10731
|
}
|
|
10213
10732
|
};
|
|
@@ -10219,30 +10738,44 @@ var DevelopmentRun = class {
|
|
|
10219
10738
|
#configPath;
|
|
10220
10739
|
#configName;
|
|
10221
10740
|
#projectDirectory;
|
|
10741
|
+
#overrides;
|
|
10222
10742
|
/** Restarts run one at a time; a change that lands during one waits its turn. */
|
|
10223
10743
|
#restartQueue = Promise.resolve();
|
|
10224
10744
|
/** The most recent change, so a queued restart can be skipped once a newer change is waiting behind it. */
|
|
10225
10745
|
#latestChange = 0;
|
|
10226
10746
|
/** When the config file was last written, as of the last time it was loaded. */
|
|
10227
10747
|
#configModified;
|
|
10228
|
-
constructor(config, configPath, projectDirectory) {
|
|
10748
|
+
constructor(config, configPath, projectDirectory, overrides) {
|
|
10229
10749
|
config.production = false;
|
|
10230
10750
|
this.#config = config;
|
|
10231
|
-
this.#app = new App(config, configPath);
|
|
10232
10751
|
this.#configPath = configPath;
|
|
10233
10752
|
this.#configName = basename(configPath);
|
|
10234
10753
|
this.#projectDirectory = projectDirectory;
|
|
10754
|
+
this.#overrides = overrides;
|
|
10755
|
+
this.#app = this.#createApp(config);
|
|
10235
10756
|
this.#configModified = this.#modifiedTime();
|
|
10236
10757
|
}
|
|
10758
|
+
#createApp(config) {
|
|
10759
|
+
const app = new App(config, this.#configPath);
|
|
10760
|
+
app.pluginData[DEVELOPMENT_SERVER_OVERRIDES] = this.#overrides;
|
|
10761
|
+
return app;
|
|
10762
|
+
}
|
|
10237
10763
|
async start() {
|
|
10238
|
-
chokidar.watch(this.#configPath, {
|
|
10764
|
+
const watcher = chokidar.watch(this.#configPath, {
|
|
10239
10765
|
ignoreInitial: true,
|
|
10240
10766
|
awaitWriteFinish: {
|
|
10241
10767
|
stabilityThreshold: 100,
|
|
10242
10768
|
pollInterval: 20
|
|
10243
10769
|
}
|
|
10244
|
-
})
|
|
10245
|
-
this.#
|
|
10770
|
+
});
|
|
10771
|
+
watcher.on("change", () => this.#queueRestart()).on("add", () => this.#queueRestart());
|
|
10772
|
+
this.#restartQueue = this.#app.run().catch(async (error) => {
|
|
10773
|
+
Log.error("Failed to start the dev server:");
|
|
10774
|
+
console.error(error instanceof Error && error.cause ? error.message : error);
|
|
10775
|
+
await watcher.close();
|
|
10776
|
+
await this.#app.close();
|
|
10777
|
+
process.exitCode = 1;
|
|
10778
|
+
});
|
|
10246
10779
|
await this.#restartQueue;
|
|
10247
10780
|
this.#restartIfModified();
|
|
10248
10781
|
}
|
|
@@ -10269,7 +10802,7 @@ var DevelopmentRun = class {
|
|
|
10269
10802
|
if (!nextConfig) return;
|
|
10270
10803
|
let nextApp;
|
|
10271
10804
|
try {
|
|
10272
|
-
nextApp =
|
|
10805
|
+
nextApp = this.#createApp(nextConfig);
|
|
10273
10806
|
} catch (error) {
|
|
10274
10807
|
Log.error("Failed to create the app, keeping the current server:");
|
|
10275
10808
|
console.error(error);
|