@jxsuite/compiler 4.0.0 → 4.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/compiler",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,10 +46,10 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@apidevtools/json-schema-ref-parser": "^16.0.2",
49
- "@jxsuite/create": "^1.3.10",
50
- "@jxsuite/runtime": "^4.0.0",
49
+ "@jxsuite/create": "^1.3.11",
50
+ "@jxsuite/runtime": "^4.0.1",
51
51
  "@jxsuite/schema": "^2.2.0",
52
- "@jxsuite/site": "^2.0.0",
52
+ "@jxsuite/site": "^2.0.1",
53
53
  "@vue/reactivity": "3.5.42",
54
54
  "esbuild": "^0.28.2",
55
55
  "lit-html": "^3.3.3",
@@ -58,7 +58,7 @@
58
58
  "devDependencies": {
59
59
  "@happy-dom/global-registrator": "^20.14.5",
60
60
  "@jxsuite/connector": "^0.6.0",
61
- "@jxsuite/parser": "^1.8.0",
61
+ "@jxsuite/parser": "^1.8.1",
62
62
  "@types/mdast": "^4.0.4"
63
63
  }
64
64
  }
package/src/shared.ts CHANGED
@@ -13,16 +13,14 @@ import {
13
13
  booleanAttrValue,
14
14
  buildStyleRules,
15
15
  enumeratedAttrNames,
16
- isDeclarationAtRule,
17
- isKeyframesAtRule,
18
16
  isNestedSelectorKey,
19
17
  camelToKebab,
20
18
  isSingleExpression,
21
- pureSchemeOf,
22
19
  splitSelectorList,
23
20
  } from "@jxsuite/runtime";
24
21
  import { evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
25
22
  import { runStatements } from "@jxsuite/runtime/statements";
23
+ import { buildSiteStyleCSS } from "@jxsuite/site/site-style";
26
24
  import {
27
25
  bodyReturnsValue,
28
26
  childrenContainArray,
@@ -1110,17 +1108,30 @@ function pushStyleRules(
1110
1108
  against a live scope, which a compiled page has only where the runtime is present, so a
1111
1109
  static emitter has always dropped it. Silently, which is the defect: a document is correct
1112
1110
  in Studio and simply unstyled in the built page, with nothing said. */
1113
- resolveValue: (property, value) => {
1114
- const source = typeof value === "string" ? value : (value as { $ref: string }).$ref;
1115
- _droppedReactive.push({ property, selector, source });
1116
- return null;
1117
- },
1111
+ resolveValue: (property, value) => recordDroppedReactive(property, value, selector),
1118
1112
  scope: selector,
1119
1113
  })) {
1120
1114
  rules.push(rule.text);
1121
1115
  }
1122
1116
  }
1123
1117
 
1118
+ /**
1119
+ * Record a reactive declaration a static build drops, and answer `null` so it is dropped.
1120
+ *
1121
+ * One recorder for both emitters the build reaches — `pushStyleRules` for element styles, and
1122
+ * `buildSiteStyleCSS` for the project block, which takes it as its resolver — so a `${…}` in
1123
+ * `project.json#/style` is reported the way one in an element's `style` is.
1124
+ */
1125
+ function recordDroppedReactive(
1126
+ property: string,
1127
+ value: string | JxRef,
1128
+ selector: string | null,
1129
+ ): null {
1130
+ const source = typeof value === "string" ? value : value.$ref;
1131
+ _droppedReactive.push({ property, selector, source });
1132
+ return null;
1133
+ }
1134
+
1124
1135
  /** One entry per reactive declaration a static build dropped, since the last drain. */
1125
1136
  const _droppedReactive: { property: string; selector: string | null; source: string }[] = [];
1126
1137
 
@@ -1188,6 +1199,21 @@ export function attrHelperSource(): string {
1188
1199
  /**
1189
1200
  * Walk the entire document tree and collect all static nested CSS rules.
1190
1201
  *
1202
+ * The project block is NOT split here. `buildSiteStyleCSS` (`@jxsuite/site/site-style`) is the one
1203
+ * definition of how `project.json#/style` becomes a page's stylesheet — custom properties and
1204
+ * `color-scheme` on `:root`, other declarations on `body`, `&`-keyed blocks as states of `:root`,
1205
+ * selector keys as their own rules, `@`-blocks resolved and dual-emitted, then the forced-scheme
1206
+ * `color-scheme` triplet — and the built page calls it with an identity transposer, so the canvas
1207
+ * and the live preview show what the build ships by construction. This function used to carry its
1208
+ * own copy of that split, and the two copies disagreed on two shapes the site builder had right: a
1209
+ * top-level `&[data-theme="light"]` reached the page as a raw `&` selector, which no engine
1210
+ * matches, and a top-level `colorScheme` went to `body`, where `light-dark()` on the root reads the
1211
+ * UA default instead (#329, spec.md §9.5).
1212
+ *
1213
+ * The one thing the build adds is the resolver: a host drops a reactive project declaration in
1214
+ * silence because it has nothing to evaluate it against, and a static build drops it for the same
1215
+ * reason but SAYS SO, through the same recorder `pushStyleRules` uses for element styles.
1216
+ *
1191
1217
  * @param {JxElement | JxMutableNode} doc
1192
1218
  * @param {Record<string, string>} [mediaQueries]
1193
1219
  * @param {JxStyle | null} [projectStyle]
@@ -1200,92 +1226,17 @@ export function compileStyles(
1200
1226
  ) {
1201
1227
  const rules: string[] = [];
1202
1228
 
1203
- // Emit project-level (site-wide) styles — CSS custom properties go on :root,
1204
- // Everything else on body. Project-level style is implicitly :root, so a
1205
- // Flat object like { "--bg": "#000", "margin": "0" } is the expected format.
1206
- if (projectStyle && typeof projectStyle === "object") {
1207
- /* The project's own style is implicitly `:root`, but only half of it: a custom property has to
1208
- land on `:root` so a forced-scheme selector can override it, while an ordinary property is
1209
- page chrome and belongs on `body`. That split is the one thing the shared builder cannot do
1210
- for us, so it is done here and each half is handed over whole. */
1211
- const rootProps: JxStyle = {};
1212
- const bodyProps: JxStyle = {};
1213
- const selectorBlocks: [string, JxStyle][] = [];
1214
- const conditionalBlocks: [string, JxStyle][] = [];
1215
- for (const [key, val] of Object.entries(projectStyle)) {
1216
- if (val !== null && typeof val === "object" && !Array.isArray(val)) {
1217
- if (key.startsWith("@")) {
1218
- conditionalBlocks.push([key, val]);
1219
- } else if (!key.startsWith("--")) {
1220
- selectorBlocks.push([key, val]);
1221
- }
1222
- continue;
1223
- }
1224
- if (isNestedSelectorKey(key) || key.startsWith("@")) {
1225
- continue;
1226
- }
1227
- if (key.startsWith("--")) {
1228
- rootProps[key] = val;
1229
- } else {
1230
- bodyProps[key] = val;
1231
- }
1232
- }
1233
-
1234
- // Base rules precede conditional blocks so equal-specificity overrides win by source order.
1235
- pushStyleRules(rules, rootProps, ":root", mediaQueries);
1236
- pushStyleRules(rules, bodyProps, "body", mediaQueries);
1237
-
1238
- for (const [key, val] of selectorBlocks) {
1239
- // A top-level selector key IS the selector — `.card` styles `.card`, not `:root .card`.
1240
- pushStyleRules(rules, val, key, mediaQueries);
1241
- }
1242
-
1243
- for (const [key, val] of conditionalBlocks) {
1244
- /* An unscoped at-rule has no selector to split across, and its name is global —
1245
- `@font-face` at project level is one block, not one per target. `@keyframes` is here for
1246
- a stronger reason than tidiness: splitting it would emit one same-named block per stop,
1247
- and the last definition of a name replaces every earlier one, so the animation would keep
1248
- only its final stop. */
1249
- if (isDeclarationAtRule(key) || isKeyframesAtRule(key)) {
1250
- pushStyleRules(rules, { [key]: val }, null, mediaQueries);
1251
- continue;
1252
- }
1253
- // Conditional block at project top level: custom properties override :root, direct
1254
- // Properties override body, selector-keyed sub-objects their own selector.
1255
- const condRoot: JxStyle = {};
1256
- const condBody: JxStyle = {};
1257
- const condSubs: [string, JxStyle][] = [];
1258
- for (const [k, v] of Object.entries(val)) {
1259
- if (v !== null && typeof v === "object" && !Array.isArray(v)) {
1260
- if (!k.startsWith("@")) {
1261
- condSubs.push([k, v]);
1262
- }
1263
- continue;
1264
- }
1265
- if (k.startsWith("--")) {
1266
- condRoot[k] = v;
1267
- } else {
1268
- condBody[k] = v;
1269
- }
1270
- }
1271
- pushStyleRules(rules, { [key]: condRoot }, ":root", mediaQueries);
1272
- pushStyleRules(rules, { [key]: condBody }, "body", mediaQueries);
1273
- for (const [sel, sub] of condSubs) {
1274
- pushStyleRules(rules, { [key]: sub }, sel, mediaQueries);
1275
- }
1276
- }
1277
- }
1278
-
1279
- // Forced-scheme UA hint: native widgets follow the forced attribute, not only the OS scheme.
1280
- if (
1281
- Object.values(mediaQueries).some((q) => pureSchemeOf(q) !== null) &&
1282
- !(projectStyle && typeof projectStyle === "object" && "colorScheme" in projectStyle)
1283
- ) {
1284
- rules.push(
1285
- ":root { color-scheme: light dark }",
1286
- `:root:where([${COLOR_SCHEME_ATTR}="light"]) { color-scheme: light }`,
1287
- `:root:where([${COLOR_SCHEME_ATTR}="dark"]) { color-scheme: dark }`,
1288
- );
1229
+ /* An absent project block is an empty one: the builder emits nothing for `{}` except the
1230
+ `color-scheme` triplet a scheme query in `$media` still calls for, which is exactly what a
1231
+ page with a scheme query and no project style has always carried. */
1232
+ const projectSheet = buildSiteStyleCSS(
1233
+ projectStyle && typeof projectStyle === "object" ? projectStyle : {},
1234
+ mediaQueries,
1235
+ (value) => value,
1236
+ recordDroppedReactive,
1237
+ );
1238
+ if (projectSheet !== "") {
1239
+ rules.push(projectSheet);
1289
1240
  }
1290
1241
 
1291
1242
  const counter = { n: 0 };
@@ -456,11 +456,24 @@ export async function buildSite(
456
456
  // The full drive path on Windows, so resolve() then wrote the sidecar back into the
457
457
  // Source tree instead of dist.
458
458
  const outName = f.tagName ? `${f.tagName}.js` : basename(f.path);
459
- writeFileSync(resolve(componentOutDir, outName), f.content, "utf8");
460
- if (f.tagName) {
459
+ /*
460
+ * Written and recorded ONCE per tag, in first-seen order. `compileElement` returns a
461
+ * component's `$elements` dependencies as extra files, so a dependency two parents name
462
+ * arrives here once per parent and once more for its own compile — and
463
+ * `injectComponentScripts` inlines a tag's stylesheet and loads its module once per
464
+ * record: three copies of `lcb-icon { display: inline-block }` in one head on a
465
+ * card → button → icon page (#330). The FIRST position is the one kept, because
466
+ * equal-specificity component rules cascade by source order and a later duplicate must
467
+ * not move a sheet that already landed. The write is skipped with the record: every
468
+ * arrival is the same file compiled with the same options (a fresh `visited` set each
469
+ * call), so the bytes are identical and a second write only inflates `fileCount`, which
470
+ * reports files produced rather than writes attempted.
471
+ */
472
+ if (!compiledComponentTags.includes(f.tagName)) {
473
+ writeFileSync(resolve(componentOutDir, outName), f.content, "utf8");
461
474
  compiledComponentTags.push(f.tagName);
475
+ fileCount += 1;
462
476
  }
463
- fileCount += 1;
464
477
  }
465
478
 
466
479
  // Pre-render component HTML scaffold and CSS sidecar