@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.
@@ -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
- if (isNodeModulesPath(join(dirname(filePath), link), this.root)) return {
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 with postcss-import, so the tag ends up self-contained. Opt-in per tag through the
3838
- * "bundle" attribute (configurable via HtmlBundleStyleOptions) on a "<link rel=stylesheet>" or a "<style>" tag.
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 [, processError] = await processCss(styleMetadata.ast, [postcssImport({ load: async (absoluteFilePath) => {
3892
- const source = relative(this.root, absoluteFilePath);
3893
- const metadata = this.findMetadata({ filePath: source });
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: metadata.id
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 source;
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 sourceRelative = join(dirname(metadata.filePath), source);
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.insertAdjacentHTML("beforeend", element.outerHTML);
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.insertAdjacentHTML("beforeend", element.outerHTML);
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
- if (atRule.name !== "import") return;
6708
- for (const match of atRule.params.matchAll(cssUrlRe)) {
6709
- const source = match[2];
6710
- if (!source) continue;
6711
- handles.push({
6712
- node: atRule,
6713
- get source() {
6714
- return source;
6715
- },
6716
- set source(newSource) {
6717
- atRule.params = atRule.params.replace(source, () => newSource);
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
  /**