@jxsuite/compiler 0.22.0 → 0.23.0

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": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,8 +38,8 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@apidevtools/json-schema-ref-parser": "^15.3.5",
41
- "@jxsuite/parser": "^0.22.0",
42
- "@jxsuite/runtime": "^0.22.0",
41
+ "@jxsuite/parser": "^0.23.0",
42
+ "@jxsuite/runtime": "^0.23.0",
43
43
  "remark-gfm": "^4.0.1",
44
44
  "remark-stringify": "^11.0.0",
45
45
  "sharp": "^0.34.5",
package/src/shared.js CHANGED
@@ -6,9 +6,17 @@
6
6
  */
7
7
 
8
8
  import { camelToKebab, toCSSText, RESERVED_KEYS } from "@jxsuite/runtime";
9
+ import { compileExpression, isMutating, evaluateExpression } from "@jxsuite/runtime/expression";
9
10
 
10
11
  // Re-export runtime utilities used by submodules
11
- export { camelToKebab, toCSSText, RESERVED_KEYS };
12
+ export {
13
+ camelToKebab,
14
+ toCSSText,
15
+ RESERVED_KEYS,
16
+ compileExpression,
17
+ isMutating,
18
+ evaluateExpression,
19
+ };
12
20
 
13
21
  // CDN defaults
14
22
  export const DEFAULT_REACTIVITY_SRC = "https://esm.sh/@vue/reactivity@3.5.32";
@@ -231,7 +239,7 @@ export function buildInitialScope(defs = {}, parentScope = null) {
231
239
  setOwnScopeValue(scope, key, cloneValue(d.default));
232
240
  continue;
233
241
  }
234
- if (!d.$prototype && !isSchemaOnly(d)) {
242
+ if (!d.$prototype && !("$expression" in d) && !isSchemaOnly(d)) {
235
243
  setOwnScopeValue(scope, key, cloneValue(d));
236
244
  }
237
245
  }
@@ -243,6 +251,17 @@ export function buildInitialScope(defs = {}, parentScope = null) {
243
251
  }
244
252
  if (!def || typeof def !== "object") continue;
245
253
  const d = /** @type {JxStateObject & JxPrototypeDef} */ (def);
254
+ if ("$expression" in d) {
255
+ const node = /** @type {any} */ (d).$expression;
256
+ if (isMutating(node.operator)) {
257
+ setOwnScopeValue(scope, key, (/** @type {any} */ s, /** @type {any} */ event) =>
258
+ evaluateExpression(node, s, event),
259
+ );
260
+ } else {
261
+ defineLazyScopeValue(scope, key, () => evaluateExpression(node, scope, null));
262
+ }
263
+ continue;
264
+ }
246
265
  if (d.$prototype === "Function") {
247
266
  if (d.body) {
248
267
  const fn = new Function("state", ...(d.parameters ?? d.arguments ?? []), d.body);
@@ -518,11 +537,11 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
518
537
  for (const [key, val] of Object.entries(obj)) {
519
538
  if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
520
539
  if (key.startsWith("@")) {
521
- const query = key.startsWith("@--")
522
- ? (mediaQueries[key.slice(1)] ?? key.slice(1))
523
- : key.slice(1);
540
+ const atRule = key.startsWith("@--")
541
+ ? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
542
+ : key;
524
543
  const mProps = toCSSText(val);
525
- if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
544
+ if (mProps) rules.push(`${atRule} { ${selector} { ${mProps} } }`);
526
545
  for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
527
546
  if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
528
547
  if (sel.startsWith("@")) continue;
@@ -532,7 +551,7 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
532
551
  ? `${selector}${sel}`
533
552
  : `${selector} ${sel}`;
534
553
  const subProps = toCSSText(sub);
535
- if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
554
+ if (subProps) rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
536
555
  }
537
556
  continue;
538
557
  }
@@ -558,10 +577,12 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
558
577
  emitProjectRules(key, val);
559
578
  } else if (key.startsWith("@")) {
560
579
  // @media block at top level
561
- const query = key.startsWith("@--")
562
- ? (mediaQueries[key.slice(1)] ?? key.slice(1))
563
- : key.slice(1);
564
- rules.push(`@media ${query} { body { ${toCSSText(/** @type {object} */ (val))} } }`);
580
+ const atRule = key.startsWith("@--")
581
+ ? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
582
+ : key.startsWith("@(")
583
+ ? `@media ${key.slice(1)}`
584
+ : key;
585
+ rules.push(`${atRule} { body { ${toCSSText(/** @type {object} */ (val))} } }`);
565
586
  }
566
587
  }
567
588
  // Collect CSS custom properties into :root {}
@@ -610,11 +631,13 @@ function emitNestedElement(selector, obj, rules, mediaQueries) {
610
631
  for (const [key, val] of Object.entries(obj)) {
611
632
  if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
612
633
  if (key.startsWith("@")) {
613
- const query = key.startsWith("@--")
614
- ? (mediaQueries[key.slice(1)] ?? key.slice(1))
615
- : key.slice(1);
634
+ const atRule = key.startsWith("@--")
635
+ ? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
636
+ : key.startsWith("@(")
637
+ ? `@media ${key.slice(1)}`
638
+ : key;
616
639
  const mProps = toCSSText(val);
617
- if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
640
+ if (mProps) rules.push(`${atRule} { ${selector} { ${mProps} } }`);
618
641
  for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
619
642
  if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
620
643
  if (sel.startsWith("@")) continue;
@@ -624,7 +647,7 @@ function emitNestedElement(selector, obj, rules, mediaQueries) {
624
647
  ? `${selector}${sel}`
625
648
  : `${selector} ${sel}`;
626
649
  const subProps = toCSSText(sub);
627
- if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
650
+ if (subProps) rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
628
651
  }
629
652
  continue;
630
653
  }
@@ -688,10 +711,13 @@ export function collectStyles(
688
711
  for (const [prop, val] of Object.entries(def.style)) {
689
712
  if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
690
713
  if (prop.startsWith("@")) {
691
- const query = prop.startsWith("@--")
692
- ? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
693
- : prop.slice(1);
694
- rules.push(`@media ${query} { ${selector} { ${toCSSText(val)} } }`);
714
+ const atRule = prop.startsWith("@--")
715
+ ? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
716
+ : prop.startsWith("@(")
717
+ ? `@media ${prop.slice(1)}`
718
+ : prop;
719
+ const atBaseProps = toCSSText(val);
720
+ if (atBaseProps) rules.push(`${atRule} { ${selector} { ${atBaseProps} } }`);
695
721
  for (const [sel, nestedRules] of Object.entries(
696
722
  /** @type {Record<string, unknown>} */ (val),
697
723
  )) {
@@ -703,7 +729,7 @@ export function collectStyles(
703
729
  : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
704
730
  ? `${selector}${sel}`
705
731
  : `${selector} ${sel}`;
706
- rules.push(`@media ${query} { ${resolved} { ${toCSSText(nestedRules)} } }`);
732
+ rules.push(`${atRule} { ${resolved} { ${toCSSText(nestedRules)} } }`);
707
733
  }
708
734
  } else {
709
735
  const resolved = prop.startsWith("&")
@@ -1038,10 +1064,12 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
1038
1064
 
1039
1065
  for (const [prop, val] of Object.entries(styleDef)) {
1040
1066
  if (prop.startsWith("@")) {
1041
- const query = prop.startsWith("@--")
1042
- ? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
1043
- : prop.slice(1);
1044
- rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {object} */ (val))} } }`);
1067
+ const atRule = prop.startsWith("@--")
1068
+ ? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
1069
+ : prop.startsWith("@(")
1070
+ ? `@media ${prop.slice(1)}`
1071
+ : prop;
1072
+ rules.push(`${atRule} { ${tagName} { ${toCSSText(/** @type {object} */ (val))} } }`);
1045
1073
  } else if (
1046
1074
  prop.startsWith(":") ||
1047
1075
  prop.startsWith(".") ||
@@ -134,7 +134,7 @@ export async function buildSite(projectRoot, options = {}) {
134
134
  for (const file of componentFiles) {
135
135
  try {
136
136
  const componentPath = resolve(componentsDir, file);
137
- const result = await compileElement(componentPath);
137
+ const result = await compileElement(componentPath, { $media: projectConfig.$media });
138
138
  for (const f of result.files) {
139
139
  const outName = f.path.includes("/")
140
140
  ? /** @type {string} */ (f.path.split("/").pop())
@@ -23,6 +23,8 @@ import {
23
23
  escapeHtml,
24
24
  DEFAULT_REACTIVITY_SRC,
25
25
  DEFAULT_LIT_HTML_SRC,
26
+ compileExpression,
27
+ isMutating,
26
28
  } from "../shared.js";
27
29
 
28
30
  /**
@@ -85,6 +87,19 @@ export function compileClient(raw, opts) {
85
87
 
86
88
  const d = /** @type {JxMutableNode} */ (def);
87
89
 
90
+ // $expression: Shape 5
91
+ if ("$expression" in d) {
92
+ const node = /** @type {any} */ (d).$expression;
93
+ if (isMutating(node.operator)) {
94
+ const compiled = compileExpression(node, { statePrefix: "state", eventParam: "e" });
95
+ onEntries.push([key, { args: ["state", "e"], body: compiled }]);
96
+ } else {
97
+ const compiled = compileExpression(node, { statePrefix: "state", eventParam: "e" });
98
+ computedEntries.push([key, `() => ${compiled}`]);
99
+ }
100
+ continue;
101
+ }
102
+
88
103
  // $prototype: "Function"
89
104
  if (d.$prototype === "Function") {
90
105
  const args = d.parameters ?? d.arguments;
@@ -274,6 +289,15 @@ function buildClientNode(def, raw, context, bindings, handlers, counter) {
274
289
  bindAttrs.push(`@${eventName}="${key}"`);
275
290
  handlers.set(key, { args: v.parameters ?? v.arguments ?? ["state", "event"], body: v.body });
276
291
  needsBind = true;
292
+ } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ (val)) {
293
+ const key = `_h${counter.h++}`;
294
+ bindAttrs.push(`@${eventName}="${key}"`);
295
+ const compiled = compileExpression(/** @type {any} */ (val).$expression, {
296
+ statePrefix: "state",
297
+ eventParam: "e",
298
+ });
299
+ handlers.set(key, { args: ["state", "e"], body: compiled });
300
+ needsBind = true;
277
301
  }
278
302
  }
279
303
 
@@ -500,6 +524,12 @@ function emitLitMapTemplate(def) {
500
524
  ) {
501
525
  const body = mapRefsToLit(/** @type {string} */ (/** @type {JxMutableNode} */ (val).body));
502
526
  attrs += " @" + eventName + "=${(e) => { " + body + " }}";
527
+ } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ (val)) {
528
+ const compiled = compileExpression(/** @type {any} */ (val).$expression, {
529
+ statePrefix: "state",
530
+ eventParam: "e",
531
+ });
532
+ attrs += " @" + eventName + "=${(e) => { state.$map = { item, index }; " + compiled + "; }}";
503
533
  }
504
534
  }
505
535
 
@@ -6,7 +6,14 @@
6
6
  */
7
7
 
8
8
  import { camelToKebab, RESERVED_KEYS } from "@jxsuite/runtime";
9
- import { escapeHtml, tagNameToClassName, isSchemaOnly, collectStyles } from "../shared.js";
9
+ import {
10
+ escapeHtml,
11
+ tagNameToClassName,
12
+ isSchemaOnly,
13
+ collectStyles,
14
+ compileExpression,
15
+ isMutating,
16
+ } from "../shared.js";
10
17
 
11
18
  /**
12
19
  * Compile a Jx custom element document to a JS module string.
@@ -16,7 +23,7 @@ import { escapeHtml, tagNameToClassName, isSchemaOnly, collectStyles } from "../
16
23
  * @returns {Promise<{ files: { path: string; content: string; tagName: string }[] }>}
17
24
  */
18
25
  export async function compileElement(sourcePath, opts = {}) {
19
- const { resolveElementPath } = /** @type {JxMutableNode} */ (opts);
26
+ const { resolveElementPath, $media: optsMedia } = /** @type {JxMutableNode} */ (opts);
20
27
  /** @type {{ path: string; content: string; tagName: string }[]} */
21
28
  const files = [];
22
29
  /** @type {Set<string>} */
@@ -82,6 +89,9 @@ export async function compileElement(sourcePath, opts = {}) {
82
89
  }
83
90
 
84
91
  const className = tagNameToClassName(tagName);
92
+ if (optsMedia) {
93
+ doc.$media = { ...optsMedia, ...doc.$media };
94
+ }
85
95
  const jsContent = emitElementModule(doc, className, elementImports);
86
96
  const outputPath = filePath ? filePath.replace(/\.json$/, ".js") : `${tagName}.js`;
87
97
  files.push({ path: outputPath, content: jsContent, tagName });
@@ -227,7 +237,14 @@ export function emitElementModule(doc, className, elementImports) {
227
237
 
228
238
  for (const [key, def] of Object.entries(defs)) {
229
239
  const d = /** @type {JxMutableNode} */ (def);
230
- if (d && typeof d === "object" && !Array.isArray(d) && d.$prototype === "Function") {
240
+ if (d && typeof d === "object" && !Array.isArray(d) && "$expression" in d) {
241
+ const node = /** @type {any} */ (d).$expression;
242
+ if (isMutating(node.operator)) {
243
+ functionEntries.push([key, d]);
244
+ } else {
245
+ computedEntries.push([key, d]);
246
+ }
247
+ } else if (d && typeof d === "object" && !Array.isArray(d) && d.$prototype === "Function") {
231
248
  if (typeof d.body === "string" && d.body.includes("return")) {
232
249
  computedEntries.push([key, d]);
233
250
  } else {
@@ -252,7 +269,10 @@ export function emitElementModule(doc, className, elementImports) {
252
269
  // Emit functions: this.state.fnName = (state) => { body } or imported $src function
253
270
  for (const [key, def] of functionEntries) {
254
271
  lines.push("");
255
- if (def.$src) {
272
+ if ("$expression" in def) {
273
+ const compiled = compileExpression(def.$expression, { statePrefix: "s", eventParam: "e" });
274
+ lines.push(` this.state.${key} = (s, e) => { ${compiled}; };`);
275
+ } else if (def.$src) {
256
276
  // $src function — wrap imported function so it receives state
257
277
  const args = def.parameters ?? def.arguments ?? ["state"];
258
278
  const paramList = args.join(", ");
@@ -269,7 +289,13 @@ export function emitElementModule(doc, className, elementImports) {
269
289
  // Emit computed signals — $src or inline body
270
290
  for (const [key, def] of computedEntries) {
271
291
  lines.push("");
272
- if (def.$src) {
292
+ if ("$expression" in def) {
293
+ const compiled = compileExpression(def.$expression, {
294
+ statePrefix: "this.state",
295
+ eventParam: "e",
296
+ });
297
+ lines.push(` this.state.${key} = computed(() => ${compiled});`);
298
+ } else if (def.$src) {
273
299
  lines.push(` this.state.${key} = computed(() => ${key}(this.state));`);
274
300
  } else {
275
301
  lines.push(` this.state.${key} = computed(() => {`);
@@ -488,6 +514,12 @@ function emitLitNode(def, indent) {
488
514
  parts.push(
489
515
  `@${eventName}="\${(e) => ${refToExpr(/** @type {string} */ (/** @type {JxMutableNode} */ (val).$ref))}(s, e)}"`,
490
516
  );
517
+ } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ (val)) {
518
+ const compiled = compileExpression(/** @type {any} */ (val).$expression, {
519
+ statePrefix: "s",
520
+ eventParam: "e",
521
+ });
522
+ parts.push(`@${eventName}="\${(e) => { ${compiled}; }}"`);
491
523
  } else if (
492
524
  val &&
493
525
  typeof val === "object" &&
@@ -558,6 +590,12 @@ function emitMappedArray(arrayDef, indent) {
558
590
  parts.push(
559
591
  `@${eventName}="\${(e) => ${refToExpr(/** @type {string} */ (/** @type {JxMutableNode} */ (val).$ref))}(s, e)}"`,
560
592
  );
593
+ } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ (val)) {
594
+ const compiled = compileExpression(/** @type {any} */ (val).$expression, {
595
+ statePrefix: "s",
596
+ eventParam: "e",
597
+ });
598
+ parts.push(`@${eventName}="\${(e) => { ${compiled}; }}"`);
561
599
  }
562
600
  }
563
601