@staticbolt/core 1.0.0-beta.28 → 1.0.0-beta.29
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/plugins/index.d.mts +7 -3
- package/lib/plugins/index.d.mts.map +1 -1
- package/lib/plugins/index.mjs +553 -57
- package/lib/plugins/index.mjs.map +1 -1
- package/package.json +3 -5
package/lib/plugins/index.mjs
CHANGED
|
@@ -22,7 +22,6 @@ import transformDefine from "babel-plugin-transform-define";
|
|
|
22
22
|
import { availableParallelism } from "node:os";
|
|
23
23
|
import { Worker } from "node:worker_threads";
|
|
24
24
|
import vm from "node:vm";
|
|
25
|
-
import postcssImport from "postcss-import";
|
|
26
25
|
import * as z$1 from "zod/v4";
|
|
27
26
|
import rehypeExternalLinks from "rehype-external-links";
|
|
28
27
|
import rehypeSlug from "rehype-slug";
|
|
@@ -698,6 +697,10 @@ function isNodeModulesPath(path, root) {
|
|
|
698
697
|
root
|
|
699
698
|
});
|
|
700
699
|
}
|
|
700
|
+
/** Directory of the package a `node_modules` path belongs to, `undefined` for paths outside `node_modules`. */
|
|
701
|
+
function packageDirectory(path) {
|
|
702
|
+
return /^(.*?node_modules\/(?:@[^/]+\/)?[^/]+)(?:\/|$)/.exec(path)?.[1];
|
|
703
|
+
}
|
|
701
704
|
/**
|
|
702
705
|
* Returns a stable key for a specifier, ignoring publicName and function refs. Side-effects are keyed by type alone (only one per
|
|
703
706
|
* source).
|
|
@@ -772,7 +775,8 @@ function bundlePackagesPlugin(options = {}) {
|
|
|
772
775
|
resolveSource(source, filePath) {
|
|
773
776
|
if (!isValidRelativePath(source)) return;
|
|
774
777
|
const [link] = splitHtmlLink(source);
|
|
775
|
-
|
|
778
|
+
const target = join(dirname(filePath), link);
|
|
779
|
+
if (isNodeModulesPath(target, this.root) && packageDirectory(target) !== packageDirectory(filePath)) return {
|
|
776
780
|
source,
|
|
777
781
|
dependencyID: void 0
|
|
778
782
|
};
|
|
@@ -844,7 +848,7 @@ function bundlePackagesPlugin(options = {}) {
|
|
|
844
848
|
existingSpecifiers.push({ type: "side-effect" });
|
|
845
849
|
specifiersByPackage.set(packageName, existingSpecifiers);
|
|
846
850
|
}
|
|
847
|
-
const styleLinks = inputMetadata.ast.querySelectorAll("link[rel=\"stylesheet\"]");
|
|
851
|
+
const styleLinks = inputMetadata.ast.querySelectorAll("link[rel=\"stylesheet\"], link[rel=\"preload\"][as=\"style\"]");
|
|
848
852
|
for (const styleLink of styleLinks) {
|
|
849
853
|
const source = styleLink.getAttribute("href");
|
|
850
854
|
if (!source) continue;
|
|
@@ -3831,11 +3835,496 @@ function htmlBundleScriptPlugin(options = {}) {
|
|
|
3831
3835
|
};
|
|
3832
3836
|
}
|
|
3833
3837
|
|
|
3838
|
+
//#endregion
|
|
3839
|
+
//#region src/ast-utilities/postcss-utils/import-prelude.ts
|
|
3840
|
+
const isSpace = (char) => char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
|
|
3841
|
+
function skipSpace(text, index) {
|
|
3842
|
+
while (index < text.length && isSpace(text[index])) index++;
|
|
3843
|
+
return index;
|
|
3844
|
+
}
|
|
3845
|
+
function skipSpaceAndComments(text, index) {
|
|
3846
|
+
for (;;) {
|
|
3847
|
+
index = skipSpace(text, index);
|
|
3848
|
+
if (!text.startsWith("/*", index)) return index;
|
|
3849
|
+
const end = text.indexOf("*/", index + 2);
|
|
3850
|
+
if (end === -1) return text.length;
|
|
3851
|
+
index = end + 2;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
/** Index just past the closing quote of the string starting at `index`, or `-1` when it never closes. */
|
|
3855
|
+
function skipString(text, index) {
|
|
3856
|
+
const quote = text[index];
|
|
3857
|
+
for (let current = index + 1; current < text.length; current++) {
|
|
3858
|
+
const char = text[current];
|
|
3859
|
+
if (char === "\\") {
|
|
3860
|
+
current++;
|
|
3861
|
+
continue;
|
|
3862
|
+
}
|
|
3863
|
+
if (char === quote) return current + 1;
|
|
3864
|
+
if (char === "\n") return -1;
|
|
3865
|
+
}
|
|
3866
|
+
return -1;
|
|
3867
|
+
}
|
|
3868
|
+
/** Index just past the `)` matching the `(` at `index`, or `-1` when it never closes. */
|
|
3869
|
+
function skipParens(text, index) {
|
|
3870
|
+
let depth = 0;
|
|
3871
|
+
for (let current = index; current < text.length; current++) {
|
|
3872
|
+
const char = text[current];
|
|
3873
|
+
if (char === "\"" || char === "'") {
|
|
3874
|
+
const end = skipString(text, current);
|
|
3875
|
+
if (end === -1) return -1;
|
|
3876
|
+
current = end - 1;
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
if (char === "(") depth++;
|
|
3880
|
+
else if (char === ")" && --depth === 0) return current + 1;
|
|
3881
|
+
}
|
|
3882
|
+
return -1;
|
|
3883
|
+
}
|
|
3884
|
+
const escapeRe = /\\(?:([\da-f]{1,6})\s?|(.))/gi;
|
|
3885
|
+
function unescapeCss(value) {
|
|
3886
|
+
if (!value.includes("\\")) return value;
|
|
3887
|
+
return value.replace(escapeRe, (_, hex, char) => hex ? String.fromCodePoint(Number.parseInt(hex, 16)) : char);
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Parses the params of an `@import` rule: the URL followed by its `layer`, `supports()` and media conditions.
|
|
3891
|
+
*
|
|
3892
|
+
* Returns `undefined` when no URL can be found or a condition is given twice.
|
|
3893
|
+
*/
|
|
3894
|
+
function parseImportPrelude(text) {
|
|
3895
|
+
const tokenStart = skipSpaceAndComments(text, 0);
|
|
3896
|
+
let urlStart;
|
|
3897
|
+
let urlEnd;
|
|
3898
|
+
let tokenEnd;
|
|
3899
|
+
const first = text[tokenStart];
|
|
3900
|
+
if (first === "\"" || first === "'") {
|
|
3901
|
+
tokenEnd = skipString(text, tokenStart);
|
|
3902
|
+
if (tokenEnd === -1) return;
|
|
3903
|
+
urlStart = tokenStart + 1;
|
|
3904
|
+
urlEnd = tokenEnd - 1;
|
|
3905
|
+
} else if (text.slice(tokenStart, tokenStart + 4).toLowerCase() === "url(") {
|
|
3906
|
+
const contentStart = skipSpace(text, tokenStart + 4);
|
|
3907
|
+
const quote = text[contentStart];
|
|
3908
|
+
let close;
|
|
3909
|
+
if (quote === "\"" || quote === "'") {
|
|
3910
|
+
const stringEnd = skipString(text, contentStart);
|
|
3911
|
+
if (stringEnd === -1) return;
|
|
3912
|
+
close = skipSpace(text, stringEnd);
|
|
3913
|
+
if (text[close] !== ")") return;
|
|
3914
|
+
urlStart = contentStart + 1;
|
|
3915
|
+
urlEnd = stringEnd - 1;
|
|
3916
|
+
} else {
|
|
3917
|
+
close = text.indexOf(")", contentStart);
|
|
3918
|
+
if (close === -1) return;
|
|
3919
|
+
urlStart = contentStart;
|
|
3920
|
+
urlEnd = close;
|
|
3921
|
+
while (urlEnd > urlStart && isSpace(text[urlEnd - 1])) urlEnd--;
|
|
3922
|
+
}
|
|
3923
|
+
tokenEnd = close + 1;
|
|
3924
|
+
} else return;
|
|
3925
|
+
const prelude = {
|
|
3926
|
+
url: unescapeCss(text.slice(urlStart, urlEnd)),
|
|
3927
|
+
urlStart,
|
|
3928
|
+
urlEnd,
|
|
3929
|
+
urlToken: text.slice(tokenStart, tokenEnd)
|
|
3930
|
+
};
|
|
3931
|
+
let position = tokenEnd;
|
|
3932
|
+
for (;;) {
|
|
3933
|
+
position = skipSpaceAndComments(text, position);
|
|
3934
|
+
if (position >= text.length) break;
|
|
3935
|
+
const rest = text.slice(position);
|
|
3936
|
+
if (/^layer(?=\(|\s|$)/i.test(rest)) {
|
|
3937
|
+
if (prelude.layer !== void 0) return;
|
|
3938
|
+
if (rest[5] === "(") {
|
|
3939
|
+
const end = skipParens(text, position + 5);
|
|
3940
|
+
if (end === -1) return;
|
|
3941
|
+
prelude.layer = text.slice(position + 6, end - 1).trim();
|
|
3942
|
+
position = end;
|
|
3943
|
+
} else {
|
|
3944
|
+
prelude.layer = "";
|
|
3945
|
+
position += 5;
|
|
3946
|
+
}
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
if (/^supports\(/i.test(rest)) {
|
|
3950
|
+
if (prelude.supports !== void 0) return;
|
|
3951
|
+
const end = skipParens(text, position + 8);
|
|
3952
|
+
if (end === -1) return;
|
|
3953
|
+
prelude.supports = text.slice(position + 9, end - 1).trim();
|
|
3954
|
+
position = end;
|
|
3955
|
+
continue;
|
|
3956
|
+
}
|
|
3957
|
+
prelude.media = rest.trim();
|
|
3958
|
+
break;
|
|
3959
|
+
}
|
|
3960
|
+
return prelude;
|
|
3961
|
+
}
|
|
3962
|
+
function hasConditions({ layer, supports, media }) {
|
|
3963
|
+
return layer !== void 0 || supports !== void 0 || media !== void 0;
|
|
3964
|
+
}
|
|
3965
|
+
/** Prints conditions back the way an `@import` prelude takes them: `layer(name) supports(condition) media`. */
|
|
3966
|
+
function formatImportConditions({ layer, supports, media }) {
|
|
3967
|
+
const parts = [];
|
|
3968
|
+
if (layer !== void 0) parts.push(layer ? `layer(${layer})` : "layer");
|
|
3969
|
+
if (supports !== void 0) parts.push(`supports(${supports})`);
|
|
3970
|
+
if (media !== void 0) parts.push(media);
|
|
3971
|
+
return parts.join(" ");
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
//#endregion
|
|
3975
|
+
//#region src/parsers/css-parser.ts
|
|
3976
|
+
function parseCssUnsafe(css, options) {
|
|
3977
|
+
return postcss.parse(css, options ?? {});
|
|
3978
|
+
}
|
|
3979
|
+
/** Parse CSS string and return the root node */
|
|
3980
|
+
const parseCss = valueOrError(parseCssUnsafe);
|
|
3981
|
+
|
|
3982
|
+
//#endregion
|
|
3983
|
+
//#region src/plugins/core-plugins/style-metadata/style-loader.ts
|
|
3984
|
+
function styleLoader(relativePath, code) {
|
|
3985
|
+
const absFilePath = join(this.root, relativePath);
|
|
3986
|
+
const [cssRoot, parseError] = parseCss(code, { from: absFilePath });
|
|
3987
|
+
if (parseError) return [null, parseError];
|
|
3988
|
+
return [{
|
|
3989
|
+
type: METADATA_TYPES.CSS,
|
|
3990
|
+
ast: cssRoot,
|
|
3991
|
+
filePath: relativePath,
|
|
3992
|
+
id: relativePath,
|
|
3993
|
+
directDependencies: /* @__PURE__ */ new Set()
|
|
3994
|
+
}, null];
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
//#endregion
|
|
3998
|
+
//#region src/helpers/style-bundler.ts
|
|
3999
|
+
const externalUrlRe = /^(?:[a-z][\d+.a-z-]*:|\/\/)/i;
|
|
4000
|
+
/**
|
|
4001
|
+
* Inlines the `@import` tree of a stylesheet in place, the way a browser would have loaded it:
|
|
4002
|
+
*
|
|
4003
|
+
* - Imported files come from the build when they are part of it, so what other plugins did to them is kept, and from disk
|
|
4004
|
+
* otherwise. Their `url()` references are rebased onto the bundle's final location.
|
|
4005
|
+
* - `layer()`, `supports()` and media conditions wrap the imported rules; nested imports nest their conditions.
|
|
4006
|
+
* - A file imported more than once under the same conditions is kept where it was imported last, which is where a browser's cascade
|
|
4007
|
+
* ends up. Cyclic imports are cut.
|
|
4008
|
+
* - Imports that cannot be inlined (other origins, `data:`, files that fail to resolve or load) are kept and hoisted to the top
|
|
4009
|
+
* with their conditions folded in, after `@charset` and any `@layer` statements that came before them.
|
|
4010
|
+
* - Misplaced imports (after other rules) are reported and left alone, like a browser ignores them.
|
|
4011
|
+
*/
|
|
4012
|
+
var StyleBundler = class {
|
|
4013
|
+
#app;
|
|
4014
|
+
/** Stylesheets outside the build, loaded from disk once and cloned per use. `undefined` marks a file that failed to load. */
|
|
4015
|
+
#loaded = /* @__PURE__ */ new Map();
|
|
4016
|
+
constructor(app) {
|
|
4017
|
+
this.#app = app;
|
|
4018
|
+
}
|
|
4019
|
+
/**
|
|
4020
|
+
* Bundles `metadata.ast` in place.
|
|
4021
|
+
*
|
|
4022
|
+
* @param bundleAbsolutePath Where the bundle ends up, which imported `url()` references are rebased onto.
|
|
4023
|
+
*/
|
|
4024
|
+
async bundle(metadata, bundleAbsolutePath) {
|
|
4025
|
+
return await new BundleRun(this.#app, this.#loaded, bundleAbsolutePath).bundle(metadata);
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
var BundleRun = class {
|
|
4029
|
+
#app;
|
|
4030
|
+
#loaded;
|
|
4031
|
+
#bundleAbsolutePath;
|
|
4032
|
+
#entries = [];
|
|
4033
|
+
#occurrences = [];
|
|
4034
|
+
#diagnostics = [];
|
|
4035
|
+
#reportedNodes = /* @__PURE__ */ new Set();
|
|
4036
|
+
#charsetParams;
|
|
4037
|
+
#hasKeptImports = false;
|
|
4038
|
+
constructor(app, loaded, bundleAbsolutePath) {
|
|
4039
|
+
this.#app = app;
|
|
4040
|
+
this.#loaded = loaded;
|
|
4041
|
+
this.#bundleAbsolutePath = bundleAbsolutePath;
|
|
4042
|
+
}
|
|
4043
|
+
async bundle(metadata) {
|
|
4044
|
+
const absolutePath = join(this.#app.root, metadata.filePath);
|
|
4045
|
+
const entry = {
|
|
4046
|
+
metadata,
|
|
4047
|
+
absolutePath,
|
|
4048
|
+
chain: [],
|
|
4049
|
+
key: absolutePath,
|
|
4050
|
+
nodes: metadata.ast.nodes.slice()
|
|
4051
|
+
};
|
|
4052
|
+
await this.#visit(entry, /* @__PURE__ */ new Set([absolutePath]));
|
|
4053
|
+
await this.#assemble(entry);
|
|
4054
|
+
return this.#diagnostics;
|
|
4055
|
+
}
|
|
4056
|
+
async #visit(occurrence, ancestors) {
|
|
4057
|
+
const nodes = occurrence.metadata.ast.nodes;
|
|
4058
|
+
const filePath = occurrence.metadata.id;
|
|
4059
|
+
/** Indices of consecutive nodes that stay as they are, flushed as one entry whenever something else comes between them. */
|
|
4060
|
+
let run = [];
|
|
4061
|
+
const flushRun = () => {
|
|
4062
|
+
if (run.length === 0) return;
|
|
4063
|
+
this.#entries.push({
|
|
4064
|
+
kind: "nodes",
|
|
4065
|
+
occurrence,
|
|
4066
|
+
indices: run
|
|
4067
|
+
});
|
|
4068
|
+
run = [];
|
|
4069
|
+
};
|
|
4070
|
+
/** Only comments, `@charset`, `@layer` statements and other imports may come before an `@import`. */
|
|
4071
|
+
let isInPrefix = true;
|
|
4072
|
+
for (const [index, node] of nodes.entries()) {
|
|
4073
|
+
if (node.type === "comment") {
|
|
4074
|
+
run.push(index);
|
|
4075
|
+
continue;
|
|
4076
|
+
}
|
|
4077
|
+
const atRule = node.type === "atrule" ? node : void 0;
|
|
4078
|
+
const name = atRule?.name.toLowerCase();
|
|
4079
|
+
if (atRule && name === "charset") {
|
|
4080
|
+
if (index === 0) {
|
|
4081
|
+
this.#noteCharset(atRule, filePath);
|
|
4082
|
+
this.#entries.push({
|
|
4083
|
+
kind: "charset",
|
|
4084
|
+
occurrence,
|
|
4085
|
+
index
|
|
4086
|
+
});
|
|
4087
|
+
} else this.#warn(atRule, filePath, "@charset must precede all other statements");
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
4090
|
+
if (atRule && name === "layer" && isInPrefix && !atRule.nodes) {
|
|
4091
|
+
if (occurrence.chain.length === 0) {
|
|
4092
|
+
flushRun();
|
|
4093
|
+
this.#entries.push({
|
|
4094
|
+
kind: "layer",
|
|
4095
|
+
occurrence,
|
|
4096
|
+
index
|
|
4097
|
+
});
|
|
4098
|
+
} else run.push(index);
|
|
4099
|
+
continue;
|
|
4100
|
+
}
|
|
4101
|
+
if (!atRule || name !== "import") {
|
|
4102
|
+
isInPrefix = false;
|
|
4103
|
+
run.push(index);
|
|
4104
|
+
continue;
|
|
4105
|
+
}
|
|
4106
|
+
const plan = await this.#planImport(atRule, occurrence, ancestors, isInPrefix);
|
|
4107
|
+
switch (plan.action) {
|
|
4108
|
+
case "leave":
|
|
4109
|
+
run.push(index);
|
|
4110
|
+
break;
|
|
4111
|
+
case "drop": break;
|
|
4112
|
+
case "keep":
|
|
4113
|
+
flushRun();
|
|
4114
|
+
this.#entries.push({
|
|
4115
|
+
kind: "import",
|
|
4116
|
+
occurrence,
|
|
4117
|
+
index,
|
|
4118
|
+
prelude: plan.prelude,
|
|
4119
|
+
chain: plan.chain
|
|
4120
|
+
});
|
|
4121
|
+
this.#hasKeptImports = true;
|
|
4122
|
+
break;
|
|
4123
|
+
case "inline":
|
|
4124
|
+
flushRun();
|
|
4125
|
+
this.#occurrences.push(plan.child);
|
|
4126
|
+
await this.#visit(plan.child, new Set(ancestors).add(plan.child.absolutePath));
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
flushRun();
|
|
4130
|
+
}
|
|
4131
|
+
/**
|
|
4132
|
+
* Decides what becomes of an `@import`: inlined, kept as an import (hoisted with its conditions folded in, so the browser
|
|
4133
|
+
* fetches it the way it did before), dropped (a cycle) or left where it is as a regular node (not valid where it stands).
|
|
4134
|
+
*/
|
|
4135
|
+
async #planImport(node, occurrence, ancestors, isInPrefix) {
|
|
4136
|
+
const filePath = occurrence.metadata.id;
|
|
4137
|
+
const prelude = parseImportPrelude(node.params);
|
|
4138
|
+
if (!prelude) {
|
|
4139
|
+
this.#warn(node, filePath, `Unable to find the URL in '${node.toString()}'`);
|
|
4140
|
+
return { action: "leave" };
|
|
4141
|
+
}
|
|
4142
|
+
if (node.nodes) {
|
|
4143
|
+
this.#warn(node, filePath, "It looks like the @import statement was not ended correctly: child nodes are attached to it");
|
|
4144
|
+
return { action: "leave" };
|
|
4145
|
+
}
|
|
4146
|
+
if (!isInPrefix) {
|
|
4147
|
+
this.#warn(node, filePath, "@import must precede all other statements (besides @charset and empty @layer)");
|
|
4148
|
+
return { action: "leave" };
|
|
4149
|
+
}
|
|
4150
|
+
const chain = hasConditions(prelude) ? [...occurrence.chain, prelude] : occurrence.chain;
|
|
4151
|
+
const keep = {
|
|
4152
|
+
action: "keep",
|
|
4153
|
+
prelude,
|
|
4154
|
+
chain
|
|
4155
|
+
};
|
|
4156
|
+
if (externalUrlRe.test(prelude.url)) return keep;
|
|
4157
|
+
const absolutePath = this.#resolve(prelude.url, occurrence.absolutePath);
|
|
4158
|
+
if (!absolutePath) {
|
|
4159
|
+
this.#error(node, filePath, `Failed to resolve "${prelude.url}"`);
|
|
4160
|
+
return keep;
|
|
4161
|
+
}
|
|
4162
|
+
if (ancestors.has(absolutePath)) return { action: "drop" };
|
|
4163
|
+
const metadata = await this.#load(absolutePath);
|
|
4164
|
+
if (!metadata) {
|
|
4165
|
+
this.#error(node, filePath, `Failed to load "${prelude.url}" as a stylesheet`);
|
|
4166
|
+
return keep;
|
|
4167
|
+
}
|
|
4168
|
+
return {
|
|
4169
|
+
action: "inline",
|
|
4170
|
+
child: {
|
|
4171
|
+
metadata,
|
|
4172
|
+
absolutePath,
|
|
4173
|
+
chain,
|
|
4174
|
+
key: [absolutePath, ...chain.map((level) => formatImportConditions(level))].join("\0"),
|
|
4175
|
+
importNode: node
|
|
4176
|
+
}
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
#resolve(url, importerAbsolutePath) {
|
|
4180
|
+
const [link] = splitHtmlLink(url);
|
|
4181
|
+
if (link.startsWith("/")) return this.#app.resolver.findFile(join(this.#app.root, link));
|
|
4182
|
+
const resolved = this.#app.resolver.resolve(link, importerAbsolutePath);
|
|
4183
|
+
if (!resolved?.exists) return;
|
|
4184
|
+
return resolved.path;
|
|
4185
|
+
}
|
|
4186
|
+
async #load(absolutePath) {
|
|
4187
|
+
const relativePath = relative(this.#app.root, absolutePath);
|
|
4188
|
+
const inBuild = this.#app.findMetadata({
|
|
4189
|
+
type: METADATA_TYPES.CSS,
|
|
4190
|
+
filePath: relativePath
|
|
4191
|
+
}) ?? this.#app.findMetadata({
|
|
4192
|
+
type: METADATA_TYPES.CSS,
|
|
4193
|
+
id: relativePath
|
|
4194
|
+
});
|
|
4195
|
+
if (inBuild) return inBuild;
|
|
4196
|
+
if (!this.#loaded.has(absolutePath)) this.#loaded.set(absolutePath, await this.#loadFromDisk(absolutePath, relativePath));
|
|
4197
|
+
return this.#loaded.get(absolutePath);
|
|
4198
|
+
}
|
|
4199
|
+
async #loadFromDisk(absolutePath, relativePath) {
|
|
4200
|
+
const content = await this.#app.read(absolutePath);
|
|
4201
|
+
if (content === void 0) return;
|
|
4202
|
+
if (content.trim() === "") return styleLoader.call(this.#app, relativePath, content)[0] ?? void 0;
|
|
4203
|
+
const metadata = await this.#app.load(relativePath, { code: content });
|
|
4204
|
+
if (isStyleMetadata(metadata)) return metadata;
|
|
4205
|
+
}
|
|
4206
|
+
#noteCharset(node, filePath) {
|
|
4207
|
+
if (this.#charsetParams === void 0) {
|
|
4208
|
+
this.#charsetParams = node.params;
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
if (node.params.toLowerCase() !== this.#charsetParams.toLowerCase()) this.#warn(node, filePath, `Incompatible @charset statements: ${node.params} is dropped for ${this.#charsetParams}`);
|
|
4212
|
+
}
|
|
4213
|
+
#warn(node, filePath, message) {
|
|
4214
|
+
this.#report("warning", node, filePath, message);
|
|
4215
|
+
}
|
|
4216
|
+
#error(node, filePath, message) {
|
|
4217
|
+
this.#report("error", node, filePath, message);
|
|
4218
|
+
}
|
|
4219
|
+
#report(level, node, filePath, message) {
|
|
4220
|
+
if (this.#reportedNodes.has(node)) return;
|
|
4221
|
+
this.#reportedNodes.add(node);
|
|
4222
|
+
this.#diagnostics.push({
|
|
4223
|
+
level,
|
|
4224
|
+
message,
|
|
4225
|
+
node,
|
|
4226
|
+
filePath
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
async #assemble(entry) {
|
|
4230
|
+
const root = entry.metadata.ast;
|
|
4231
|
+
const lastByKey = /* @__PURE__ */ new Map();
|
|
4232
|
+
for (const occurrence of this.#occurrences) lastByKey.set(occurrence.key, occurrence);
|
|
4233
|
+
let charset;
|
|
4234
|
+
const prefix = [];
|
|
4235
|
+
const body = [];
|
|
4236
|
+
const started = /* @__PURE__ */ new Set();
|
|
4237
|
+
for (const item of this.#entries) {
|
|
4238
|
+
const { occurrence } = item;
|
|
4239
|
+
if (occurrence !== entry && lastByKey.get(occurrence.key) !== occurrence) continue;
|
|
4240
|
+
const nodes = await this.#nodesOf(occurrence);
|
|
4241
|
+
switch (item.kind) {
|
|
4242
|
+
case "charset":
|
|
4243
|
+
charset ??= nodes[item.index];
|
|
4244
|
+
break;
|
|
4245
|
+
case "layer":
|
|
4246
|
+
(this.#hasKeptImports ? prefix : body).push(nodes[item.index]);
|
|
4247
|
+
break;
|
|
4248
|
+
case "import": {
|
|
4249
|
+
const node = nodes[item.index];
|
|
4250
|
+
node.params = keptImportPrelude(node.params, item.prelude, item.chain);
|
|
4251
|
+
prefix.push(node);
|
|
4252
|
+
break;
|
|
4253
|
+
}
|
|
4254
|
+
case "nodes": {
|
|
4255
|
+
const run = item.indices.map((index) => nodes[index]);
|
|
4256
|
+
if (occurrence.importNode && !started.has(occurrence)) {
|
|
4257
|
+
const isBundleStart = !charset && prefix.length === 0 && body.length === 0;
|
|
4258
|
+
run[0].raws.before = isBundleStart ? "" : occurrence.importNode.raws.before || "\n";
|
|
4259
|
+
}
|
|
4260
|
+
started.add(occurrence);
|
|
4261
|
+
body.push(...wrapInConditions(run, occurrence.chain));
|
|
4262
|
+
break;
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
if (charset) prefix.unshift(charset);
|
|
4267
|
+
root.removeAll();
|
|
4268
|
+
root.append([...prefix, ...body]);
|
|
4269
|
+
}
|
|
4270
|
+
async #nodesOf(occurrence) {
|
|
4271
|
+
if (occurrence.nodes) return occurrence.nodes;
|
|
4272
|
+
const clone = this.#app.clone(occurrence.metadata);
|
|
4273
|
+
await this.#app.rebase(clone, this.#bundleAbsolutePath);
|
|
4274
|
+
occurrence.nodes = clone.ast.nodes.slice();
|
|
4275
|
+
return occurrence.nodes;
|
|
4276
|
+
}
|
|
4277
|
+
};
|
|
4278
|
+
/** Nests `nodes` in `@media`, `@supports` and `@layer` rules for every level of the chain, outermost level outermost. */
|
|
4279
|
+
function wrapInConditions(nodes, chain) {
|
|
4280
|
+
if (chain.length === 0) return nodes;
|
|
4281
|
+
const before = nodes[0].raws.before;
|
|
4282
|
+
nodes[0].raws.before = "\n";
|
|
4283
|
+
let wrapped = nodes;
|
|
4284
|
+
for (let level = chain.length - 1; level >= 0; level--) {
|
|
4285
|
+
const { layer, supports, media } = chain[level];
|
|
4286
|
+
const wrappers = [];
|
|
4287
|
+
if (layer !== void 0) wrappers.push(postcss.atRule({
|
|
4288
|
+
name: "layer",
|
|
4289
|
+
params: layer
|
|
4290
|
+
}));
|
|
4291
|
+
if (supports !== void 0) wrappers.push(postcss.atRule({
|
|
4292
|
+
name: "supports",
|
|
4293
|
+
params: `(${supports})`
|
|
4294
|
+
}));
|
|
4295
|
+
if (media !== void 0) wrappers.push(postcss.atRule({
|
|
4296
|
+
name: "media",
|
|
4297
|
+
params: media
|
|
4298
|
+
}));
|
|
4299
|
+
for (const wrapper of wrappers) {
|
|
4300
|
+
wrapper.raws = {
|
|
4301
|
+
before: "\n",
|
|
4302
|
+
between: " ",
|
|
4303
|
+
after: "\n"
|
|
4304
|
+
};
|
|
4305
|
+
wrapper.append(wrapped);
|
|
4306
|
+
wrapped = [wrapper];
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
wrapped[0].raws.before = before;
|
|
4310
|
+
return wrapped;
|
|
4311
|
+
}
|
|
4312
|
+
/**
|
|
4313
|
+
* The prelude of an import kept in the bundle, with the conditions of the imports leading to it folded in. A single level fits in
|
|
4314
|
+
* one prelude; more than one nests `data:` stylesheets, each importing the next under its own conditions, outermost first.
|
|
4315
|
+
*/
|
|
4316
|
+
function keptImportPrelude(text, prelude, chain) {
|
|
4317
|
+
if (chain.length === 0 || chain.length === 1 && chain[0] === prelude) return text;
|
|
4318
|
+
let result = `${parseImportPrelude(text)?.urlToken ?? prelude.urlToken} ${formatImportConditions(chain.at(-1))}`;
|
|
4319
|
+
for (let level = chain.length - 2; level >= 0; level--) result = `'data:text/css;base64,${Buffer.from(`@import ${result};`).toString("base64")}' ${formatImportConditions(chain[level])}`;
|
|
4320
|
+
return result;
|
|
4321
|
+
}
|
|
4322
|
+
|
|
3834
4323
|
//#endregion
|
|
3835
4324
|
//#region src/plugins/html-bundle-style/html-bundle-style-plugin.ts
|
|
3836
4325
|
/**
|
|
3837
|
-
* Inlines the "@import" tree of a stylesheet
|
|
3838
|
-
*
|
|
4326
|
+
* Inlines the "@import" tree of a stylesheet, so the tag ends up self-contained. Opt-in per tag through the "bundle" attribute
|
|
4327
|
+
* (configurable via HtmlBundleStyleOptions) on a "<link rel=stylesheet>" or a "<style>" tag.
|
|
3839
4328
|
*
|
|
3840
4329
|
* Bundling happens in place: a "<style>" tag gets the resolved CSS as its content, and a linked file is rewritten with its
|
|
3841
4330
|
* imports inlined while "href" stays as it is.
|
|
@@ -3846,15 +4335,23 @@ function htmlBundleScriptPlugin(options = {}) {
|
|
|
3846
4335
|
* - "<link rel=preload as=style>" is always bundled, with or without the attribute, since a preloaded stylesheet has to carry
|
|
3847
4336
|
* everything it needs.
|
|
3848
4337
|
* - A linked stylesheet is bundled once even when several pages link to it; they share the same file.
|
|
4338
|
+
* - Imports resolve the way every other source does: relative paths, path aliases and packages, through the build's resolver.
|
|
3849
4339
|
* - Imported files are pulled from the build (so transforms other plugins made are included) and only read from disk when they are
|
|
3850
|
-
* not part of it. They stay in the output as separate files, now unreferenced.
|
|
4340
|
+
* not part of it. They stay in the output as separate files, now unreferenced. Their "url()" references are rebased onto the
|
|
4341
|
+
* bundle's location.
|
|
4342
|
+
* - Conditions ("layer", "supports()", media) wrap the imported rules, a file imported twice under the same conditions is kept
|
|
4343
|
+
* where it was imported last, as in a browser, and imports from other origins are hoisted to the top.
|
|
3851
4344
|
*/
|
|
3852
4345
|
function htmlBundleStylePlugin(options = {}) {
|
|
3853
4346
|
const bundleAttribute = options.bundleAttribute ?? "bundle";
|
|
3854
4347
|
const query = `link[rel="stylesheet"][${bundleAttribute}], link[rel="preload"][as="style"], style[${bundleAttribute}]`;
|
|
3855
4348
|
const alreadyBundledIds = /* @__PURE__ */ new Set();
|
|
4349
|
+
let bundler;
|
|
3856
4350
|
return {
|
|
3857
4351
|
name: "html-bundle-style",
|
|
4352
|
+
setup() {
|
|
4353
|
+
bundler = new StyleBundler(this);
|
|
4354
|
+
},
|
|
3858
4355
|
async transform(metadata) {
|
|
3859
4356
|
if (!this.production) return;
|
|
3860
4357
|
if (!isHtmlMetadata(metadata)) return;
|
|
@@ -3888,16 +4385,13 @@ function htmlBundleStylePlugin(options = {}) {
|
|
|
3888
4385
|
if (alreadyBundledIds.has(styleMetadata.id)) continue;
|
|
3889
4386
|
alreadyBundledIds.add(styleMetadata.id);
|
|
3890
4387
|
}
|
|
3891
|
-
const
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
if (!metadata) return readFileSync(absoluteFilePath, "utf8");
|
|
3895
|
-
return await this.stringify(metadata);
|
|
3896
|
-
} })], { from: resolve(this.root, metadata.filePath) });
|
|
3897
|
-
if (processError !== null) printFmtError(processError, {
|
|
4388
|
+
const bundleAbsolutePath = join(this.root, isLinkTag ? styleMetadata.filePath : metadata.filePath);
|
|
4389
|
+
const diagnostics = await bundler.bundle(styleMetadata, bundleAbsolutePath);
|
|
4390
|
+
for (const { level, message, node: importNode, filePath } of diagnostics) printFmtError(message, {
|
|
3898
4391
|
function: htmlBundleStylePlugin,
|
|
3899
|
-
node,
|
|
3900
|
-
filePath
|
|
4392
|
+
node: importNode,
|
|
4393
|
+
filePath,
|
|
4394
|
+
level
|
|
3901
4395
|
});
|
|
3902
4396
|
}
|
|
3903
4397
|
},
|
|
@@ -4440,6 +4934,8 @@ function htmlInlineTextPlugin(options = {}) {
|
|
|
4440
4934
|
const noEscapeAttribute = options.noEscapeAttribute ?? "no-escape";
|
|
4441
4935
|
const cache = /* @__PURE__ */ new Map();
|
|
4442
4936
|
const dependencies = new DependencyTracker();
|
|
4937
|
+
/** Suffix on the tag's source while it travels through resolution, keeping it apart from real build sources. */
|
|
4938
|
+
const MARKER = "?html-inline-text";
|
|
4443
4939
|
return {
|
|
4444
4940
|
name: "html-inline-text",
|
|
4445
4941
|
sourcesProvider(metadata) {
|
|
@@ -4449,9 +4945,11 @@ function htmlInlineTextPlugin(options = {}) {
|
|
|
4449
4945
|
for (const inlineTag of inlineTags) {
|
|
4450
4946
|
const source = inlineTag.getAttribute(sourceAttribute);
|
|
4451
4947
|
if (!source) continue;
|
|
4948
|
+
const [link] = splitHtmlLink(source);
|
|
4949
|
+
inlineTag.setAttribute(sourceAttribute, link + MARKER);
|
|
4452
4950
|
result.push({
|
|
4453
4951
|
get source() {
|
|
4454
|
-
return
|
|
4952
|
+
return inlineTag.getAttribute(sourceAttribute) ?? "";
|
|
4455
4953
|
},
|
|
4456
4954
|
set source(newValue) {
|
|
4457
4955
|
inlineTag.setAttribute(sourceAttribute, newValue);
|
|
@@ -4460,6 +4958,17 @@ function htmlInlineTextPlugin(options = {}) {
|
|
|
4460
4958
|
}
|
|
4461
4959
|
return result;
|
|
4462
4960
|
},
|
|
4961
|
+
resolveSource(source, filePath) {
|
|
4962
|
+
const [, suffix] = splitHtmlLink(source);
|
|
4963
|
+
if (suffix !== MARKER) return;
|
|
4964
|
+
const resolved = this.resolver.resolve(source, filePath);
|
|
4965
|
+
if (!resolved?.exists) return;
|
|
4966
|
+
this.watcher?.add(relative(this.root, resolved.path));
|
|
4967
|
+
return {
|
|
4968
|
+
source: relative(join(this.root, dirname(filePath)), resolved.path),
|
|
4969
|
+
dependencyID: void 0
|
|
4970
|
+
};
|
|
4971
|
+
},
|
|
4463
4972
|
transform(metadata) {
|
|
4464
4973
|
if (!isHtmlMetadata(metadata)) return;
|
|
4465
4974
|
const inlineTags = metadata.ast.querySelectorAll(tag);
|
|
@@ -4476,7 +4985,8 @@ function htmlInlineTextPlugin(options = {}) {
|
|
|
4476
4985
|
printFmtError(`Missing "${sourceAttribute}" attribute`);
|
|
4477
4986
|
continue;
|
|
4478
4987
|
}
|
|
4479
|
-
const
|
|
4988
|
+
const [link] = splitHtmlLink(source);
|
|
4989
|
+
const sourceRelative = join(dirname(metadata.filePath), link);
|
|
4480
4990
|
let contents;
|
|
4481
4991
|
if (cache.has(sourceRelative)) contents = cache.get(sourceRelative);
|
|
4482
4992
|
else {
|
|
@@ -4953,6 +5463,14 @@ function htmlLayoutPlugin(options = {}) {
|
|
|
4953
5463
|
}
|
|
4954
5464
|
for (const dependency of layoutHtmlMetadata.directDependencies) metadata.directDependencies.add(dependency);
|
|
4955
5465
|
await this.rebase(layoutHtmlMetadata, join(this.root, metadata.filePath));
|
|
5466
|
+
const filledSlots = /* @__PURE__ */ new Set();
|
|
5467
|
+
const fillSlot = (slot, html) => {
|
|
5468
|
+
if (!filledSlots.has(slot)) {
|
|
5469
|
+
filledSlots.add(slot);
|
|
5470
|
+
slot.innerHTML = "";
|
|
5471
|
+
}
|
|
5472
|
+
slot.insertAdjacentHTML("beforeend", html);
|
|
5473
|
+
};
|
|
4956
5474
|
const children = node.querySelectorAll(":scope > *");
|
|
4957
5475
|
for (const element of children) {
|
|
4958
5476
|
if (element.nodeType !== NodeType.ELEMENT_NODE) continue;
|
|
@@ -4962,7 +5480,7 @@ function htmlLayoutPlugin(options = {}) {
|
|
|
4962
5480
|
printFmtError(`No default slot in layout file:`, layoutRelativePath, { node: element });
|
|
4963
5481
|
continue;
|
|
4964
5482
|
}
|
|
4965
|
-
for (const defaultSlot of defaultSlots) defaultSlot
|
|
5483
|
+
for (const defaultSlot of defaultSlots) fillSlot(defaultSlot, element.outerHTML);
|
|
4966
5484
|
continue;
|
|
4967
5485
|
}
|
|
4968
5486
|
const slotName = element.getAttribute("slot");
|
|
@@ -4973,7 +5491,7 @@ function htmlLayoutPlugin(options = {}) {
|
|
|
4973
5491
|
continue;
|
|
4974
5492
|
}
|
|
4975
5493
|
element.removeAttribute("slot");
|
|
4976
|
-
for (const namedSlot of namedSlots) namedSlot
|
|
5494
|
+
for (const namedSlot of namedSlots) fillSlot(namedSlot, element.outerHTML);
|
|
4977
5495
|
}
|
|
4978
5496
|
for (const slot of layoutHtmlMetadata.ast.querySelectorAll("slot")) slot.replaceWith(slot.innerHTML.trim());
|
|
4979
5497
|
mergeMaps(metadata.scriptsMetadataList, layoutHtmlMetadata.scriptsMetadataList);
|
|
@@ -6703,48 +7221,26 @@ function parseStyleSources(ast) {
|
|
|
6703
7221
|
});
|
|
6704
7222
|
}
|
|
6705
7223
|
});
|
|
6706
|
-
ast.walkAtRules((atRule) => {
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
7224
|
+
ast.walkAtRules(/^import$/i, (atRule) => {
|
|
7225
|
+
const prelude = parseImportPrelude(atRule.params);
|
|
7226
|
+
if (!prelude?.url) return;
|
|
7227
|
+
const { urlStart } = prelude;
|
|
7228
|
+
let { url, urlEnd } = prelude;
|
|
7229
|
+
handles.push({
|
|
7230
|
+
node: atRule,
|
|
7231
|
+
get source() {
|
|
7232
|
+
return url;
|
|
7233
|
+
},
|
|
7234
|
+
set source(newSource) {
|
|
7235
|
+
atRule.params = atRule.params.slice(0, urlStart) + newSource + atRule.params.slice(urlEnd);
|
|
7236
|
+
urlEnd = urlStart + newSource.length;
|
|
7237
|
+
url = newSource;
|
|
7238
|
+
}
|
|
7239
|
+
});
|
|
6721
7240
|
});
|
|
6722
7241
|
return handles;
|
|
6723
7242
|
}
|
|
6724
7243
|
|
|
6725
|
-
//#endregion
|
|
6726
|
-
//#region src/parsers/css-parser.ts
|
|
6727
|
-
function parseCssUnsafe(css, options) {
|
|
6728
|
-
return postcss.parse(css, options ?? {});
|
|
6729
|
-
}
|
|
6730
|
-
/** Parse CSS string and return the root node */
|
|
6731
|
-
const parseCss = valueOrError(parseCssUnsafe);
|
|
6732
|
-
|
|
6733
|
-
//#endregion
|
|
6734
|
-
//#region src/plugins/core-plugins/style-metadata/style-loader.ts
|
|
6735
|
-
function styleLoader(relativePath, code) {
|
|
6736
|
-
const absFilePath = join(this.root, relativePath);
|
|
6737
|
-
const [cssRoot, parseError] = parseCss(code, { from: absFilePath });
|
|
6738
|
-
if (parseError) return [null, parseError];
|
|
6739
|
-
return [{
|
|
6740
|
-
type: METADATA_TYPES.CSS,
|
|
6741
|
-
ast: cssRoot,
|
|
6742
|
-
filePath: relativePath,
|
|
6743
|
-
id: relativePath,
|
|
6744
|
-
directDependencies: /* @__PURE__ */ new Set()
|
|
6745
|
-
}, null];
|
|
6746
|
-
}
|
|
6747
|
-
|
|
6748
7244
|
//#endregion
|
|
6749
7245
|
//#region src/plugins/core-plugins/style-metadata/style-metadata-plugin.ts
|
|
6750
7246
|
/**
|