@jxsuite/compiler 2.0.7 → 2.0.8
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/dist/cli.js +155 -156
- package/dist/compiler.js +78 -79
- package/package.json +6 -6
- package/src/shared.ts +70 -27
- package/src/site/client-runtime.ts +17 -8
- package/src/site/site-build.ts +26 -10
- package/src/targets/compile-element.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@apidevtools/json-schema-ref-parser": "^16.0.0",
|
|
49
|
-
"@jxsuite/create": "^1.3.
|
|
50
|
-
"@jxsuite/runtime": "^2.0
|
|
51
|
-
"@jxsuite/schema": "^1.
|
|
49
|
+
"@jxsuite/create": "^1.3.7",
|
|
50
|
+
"@jxsuite/runtime": "^2.1.0",
|
|
51
|
+
"@jxsuite/schema": "^1.9.0",
|
|
52
52
|
"@vue/reactivity": "3.5.41",
|
|
53
53
|
"esbuild": "^0.28.2",
|
|
54
54
|
"lit-html": "^3.3.3",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@happy-dom/global-registrator": "^20.11.6",
|
|
59
|
-
"@jxsuite/connector": "^0.5.
|
|
60
|
-
"@jxsuite/parser": "^1.5.
|
|
59
|
+
"@jxsuite/connector": "^0.5.5",
|
|
60
|
+
"@jxsuite/parser": "^1.5.6",
|
|
61
61
|
"@types/mdast": "^4.0.4"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/src/shared.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
RESERVED_KEYS,
|
|
13
13
|
camelToKebab,
|
|
14
14
|
pureSchemeOf,
|
|
15
|
+
resolveAtQuery,
|
|
15
16
|
schemeSelectors,
|
|
16
17
|
toCSSText,
|
|
17
18
|
} from "@jxsuite/runtime";
|
|
@@ -779,6 +780,37 @@ function readsRuntimeOnlyState(str: string, scope: Record<string, unknown>) {
|
|
|
779
780
|
return false;
|
|
780
781
|
}
|
|
781
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Whether the whole string is exactly ONE `${…}` — the interpolation that opens at index 0 also
|
|
785
|
+
* closes at the final character.
|
|
786
|
+
*
|
|
787
|
+
* A greedy `/^\$\{(.+)\}$/` also matched `"${a} / ${b}"`, spliced the interior into `return (a} /
|
|
788
|
+
* ${b)`, and the SyntaxError became a silent null: the node rendered empty, and a `$head` entry
|
|
789
|
+
* shipped its own template text. A brace-depth scan is exact where a tightened regex is not —
|
|
790
|
+
* `"${`${a}-x`}"` is still one expression, and must keep returning a raw value.
|
|
791
|
+
*
|
|
792
|
+
* @param {string} str
|
|
793
|
+
* @returns {boolean}
|
|
794
|
+
*/
|
|
795
|
+
export function isSingleExpression(str: string): boolean {
|
|
796
|
+
if (!str.startsWith("${") || !str.endsWith("}")) {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
let depth = 0;
|
|
800
|
+
for (let i = 1; i < str.length; i += 1) {
|
|
801
|
+
const c = str[i];
|
|
802
|
+
if (c === "{") {
|
|
803
|
+
depth += 1;
|
|
804
|
+
} else if (c === "}") {
|
|
805
|
+
depth -= 1;
|
|
806
|
+
if (depth === 0) {
|
|
807
|
+
return i === str.length - 1;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
|
|
782
814
|
/**
|
|
783
815
|
* @param {string} str
|
|
784
816
|
* @param {Record<string, unknown>} scope
|
|
@@ -799,8 +831,7 @@ export function evaluateStaticTemplate(str: string, scope: Record<string, unknow
|
|
|
799
831
|
const args = ["state", "$map", "$site", "$page"] as const;
|
|
800
832
|
const values = [scope, scope?.$map, scope?.$site, scope?.$page];
|
|
801
833
|
try {
|
|
802
|
-
const
|
|
803
|
-
const body = singleExprMatch ? `return (${singleExprMatch[1]})` : `return \`${str}\``;
|
|
834
|
+
const body = isSingleExpression(str) ? `return (${str.slice(2, -1)})` : `return \`${str}\``;
|
|
804
835
|
const fn = new Function(...args, body) as (...a: unknown[]) => unknown;
|
|
805
836
|
return fn(...values);
|
|
806
837
|
} catch {
|
|
@@ -1013,11 +1044,7 @@ function conditionalRuleEmitter(
|
|
|
1013
1044
|
mediaQueries: Record<string, string>,
|
|
1014
1045
|
rules: string[],
|
|
1015
1046
|
): (selector: string, props: string) => void {
|
|
1016
|
-
const query = atKey
|
|
1017
|
-
? (mediaQueries[atKey.slice(1)] ?? atKey.slice(1))
|
|
1018
|
-
: atKey.startsWith("@(")
|
|
1019
|
-
? atKey.slice(1)
|
|
1020
|
-
: null;
|
|
1047
|
+
const query = resolveAtQuery(atKey, mediaQueries);
|
|
1021
1048
|
const atRule = query === null ? atKey : `@media ${query}`;
|
|
1022
1049
|
const scheme = query === null ? null : pureSchemeOf(query);
|
|
1023
1050
|
return (selector: string, props: string) => {
|
|
@@ -1513,6 +1540,40 @@ export function resolveStaticTagName(
|
|
|
1513
1540
|
return expression.cases[String(key)] ?? expression.default;
|
|
1514
1541
|
}
|
|
1515
1542
|
|
|
1543
|
+
/**
|
|
1544
|
+
* The content of one node: `textContent`, else `innerHTML`, else rendered `children`.
|
|
1545
|
+
*
|
|
1546
|
+
* Shared with `preRenderComponentHtml`, which returns a component's innerHTML and so cannot call
|
|
1547
|
+
* `renderStaticNode` (that would wrap the result in the host tag). It used to look at `children`
|
|
1548
|
+
* alone and return "" for anything else, so a component whose content is root-level `textContent` —
|
|
1549
|
+
* the shape spec.md §17.2 recommends when every child is a bare string — prerendered to nothing.
|
|
1550
|
+
*
|
|
1551
|
+
* @param {JxElement | JxMutableNode} node
|
|
1552
|
+
* @param {Record<string, unknown> | null} scope
|
|
1553
|
+
* @param {string | null} slotContent
|
|
1554
|
+
* @returns {string}
|
|
1555
|
+
*/
|
|
1556
|
+
function renderInner(
|
|
1557
|
+
node: JxElement | JxMutableNode,
|
|
1558
|
+
scope: Record<string, unknown> | null,
|
|
1559
|
+
slotContent: string | null,
|
|
1560
|
+
): string {
|
|
1561
|
+
if (node.textContent !== undefined) {
|
|
1562
|
+
const val = resolveStaticValue(node.textContent, scope);
|
|
1563
|
+
return val != null ? escapeHtml(String(val)) : "";
|
|
1564
|
+
}
|
|
1565
|
+
if (node.innerHTML) {
|
|
1566
|
+
const val = resolveStaticValue(node.innerHTML, scope);
|
|
1567
|
+
return val != null ? String(val) : (node.innerHTML as string);
|
|
1568
|
+
}
|
|
1569
|
+
if (Array.isArray(node.children)) {
|
|
1570
|
+
return node.children
|
|
1571
|
+
.map((c: JxElement | JxMutableNode | string) => renderStaticNode(c, scope, slotContent))
|
|
1572
|
+
.join("\n");
|
|
1573
|
+
}
|
|
1574
|
+
return "";
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1516
1577
|
/**
|
|
1517
1578
|
* Recursively render a Jx node tree to static HTML for pre-rendering.
|
|
1518
1579
|
*
|
|
@@ -1587,20 +1648,7 @@ export function renderStaticNode(
|
|
|
1587
1648
|
return `<${tag}${attrs}>`;
|
|
1588
1649
|
}
|
|
1589
1650
|
|
|
1590
|
-
|
|
1591
|
-
if (node.textContent !== undefined) {
|
|
1592
|
-
const val = resolveStaticValue(node.textContent, scope);
|
|
1593
|
-
inner = val != null ? escapeHtml(String(val)) : "";
|
|
1594
|
-
} else if (node.innerHTML) {
|
|
1595
|
-
const val = resolveStaticValue(node.innerHTML, scope);
|
|
1596
|
-
inner = val != null ? String(val) : (node.innerHTML as string);
|
|
1597
|
-
} else if (Array.isArray(node.children)) {
|
|
1598
|
-
inner = node.children
|
|
1599
|
-
.map((c: JxElement | JxMutableNode | string) => renderStaticNode(c, scope, slotContent))
|
|
1600
|
-
.join("\n");
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
1651
|
+
return `<${tag}${attrs}>${renderInner(node, scope, slotContent)}</${tag}>`;
|
|
1604
1652
|
}
|
|
1605
1653
|
|
|
1606
1654
|
/**
|
|
@@ -1636,12 +1684,7 @@ export function preRenderComponentHtml(
|
|
|
1636
1684
|
}
|
|
1637
1685
|
}
|
|
1638
1686
|
const scope = buildInitialScope(stateDefs, null);
|
|
1639
|
-
|
|
1640
|
-
return "";
|
|
1641
|
-
}
|
|
1642
|
-
return doc.children
|
|
1643
|
-
.map((c: JxElement | JxMutableNode | string) => renderStaticNode(c, scope, slotContent))
|
|
1644
|
-
.join("\n");
|
|
1687
|
+
return renderInner(doc, scope, slotContent);
|
|
1645
1688
|
}
|
|
1646
1689
|
|
|
1647
1690
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @docs framework/build
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import { dirname, join, relative, resolve } from "node:path";
|
|
21
21
|
import type { BundleResolver } from "./bundler.ts";
|
|
@@ -84,7 +84,15 @@ export interface ClientRuntime {
|
|
|
84
84
|
* @param {string} [resolveDir] - Directory bare specifiers resolve from; defaults to this package
|
|
85
85
|
* @returns {ClientRuntime}
|
|
86
86
|
*/
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* This package's own directory, resolved under both runtimes. `import.meta.dir` is Bun-only and is
|
|
89
|
+
* `undefined` under Node — and `bin/jx.js` carries a Node shebang, so every `bunx jx build` took
|
|
90
|
+
* that path, `join(undefined, …)` threw, the bare catch read it as "cannot resolve", and the import
|
|
91
|
+
* map fell back to the esm.sh CDN. `import.meta.dirname` is defined on both.
|
|
92
|
+
*/
|
|
93
|
+
const PACKAGE_DIR = import.meta.dirname;
|
|
94
|
+
|
|
95
|
+
export function resolveClientRuntime(resolveDir: string = PACKAGE_DIR): ClientRuntime {
|
|
88
96
|
const imports: Record<string, string> = {};
|
|
89
97
|
const warnings: string[] = [];
|
|
90
98
|
const assetPaths: string[] = [];
|
|
@@ -122,7 +130,7 @@ export function resolveClientRuntime(resolveDir: string = import.meta.dir): Clie
|
|
|
122
130
|
export async function writeClientRuntime(
|
|
123
131
|
runtime: ClientRuntime,
|
|
124
132
|
outDir: string,
|
|
125
|
-
resolveDir: string =
|
|
133
|
+
resolveDir: string = PACKAGE_DIR,
|
|
126
134
|
): Promise<{ written: number; errors: string[] }> {
|
|
127
135
|
const errors: string[] = [];
|
|
128
136
|
let written = 0;
|
|
@@ -212,7 +220,7 @@ const MAX_SUBPATH_PASSES = 10;
|
|
|
212
220
|
*/
|
|
213
221
|
export async function writeRuntimeSubpaths(
|
|
214
222
|
outDir: string,
|
|
215
|
-
resolveDir: string =
|
|
223
|
+
resolveDir: string = PACKAGE_DIR,
|
|
216
224
|
): Promise<{ written: number; errors: string[] }> {
|
|
217
225
|
const errors: string[] = [];
|
|
218
226
|
const done = new Set<string>();
|
|
@@ -235,7 +243,7 @@ export async function writeRuntimeSubpaths(
|
|
|
235
243
|
|
|
236
244
|
/** `from "lit-html/directives/class-map.js"` — the import forms a bundler emits. */
|
|
237
245
|
const importedSpecifiers = async (file: string): Promise<string[]> => {
|
|
238
|
-
const text =
|
|
246
|
+
const text = readFileSync(file, "utf8");
|
|
239
247
|
return [...text.matchAll(/(?:from|import)\s*["']([^"']+)["']/g)].map((m) => m[1] as string);
|
|
240
248
|
};
|
|
241
249
|
|
|
@@ -243,9 +251,10 @@ export async function writeRuntimeSubpaths(
|
|
|
243
251
|
let discovered = 0;
|
|
244
252
|
let files: string[] = [];
|
|
245
253
|
try {
|
|
246
|
-
files =
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
files = readdirSync(assetsDir, { recursive: true })
|
|
255
|
+
.map(String)
|
|
256
|
+
.filter((entry) => entry.endsWith(".js"))
|
|
257
|
+
.map((entry) => join(assetsDir, entry));
|
|
249
258
|
} catch {
|
|
250
259
|
return finish(outDir, mirrors, errors, written);
|
|
251
260
|
}
|
package/src/site/site-build.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
statSync,
|
|
19
19
|
writeFileSync,
|
|
20
20
|
} from "node:fs";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
21
22
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
22
23
|
import { isMappedArray, isRef } from "@jxsuite/schema/guards";
|
|
23
24
|
import { isNpmSpecifier, npmAssetPath, sidecarAssetPath } from "@jxsuite/schema/asset-paths";
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
colorSchemePrePaintScript,
|
|
64
65
|
evaluateStaticTemplate,
|
|
65
66
|
isComponentFullyStatic,
|
|
67
|
+
isSingleExpression,
|
|
66
68
|
isTemplateString,
|
|
67
69
|
preRenderComponentHtml,
|
|
68
70
|
pureSchemeOf,
|
|
@@ -180,7 +182,11 @@ export async function buildSite(
|
|
|
180
182
|
const elementBundles = new Map<string, string[]>();
|
|
181
183
|
const registerElementBundle = (specifiers: string[]): string => {
|
|
182
184
|
const sorted = specifiers.toSorted();
|
|
183
|
-
|
|
185
|
+
/*
|
|
186
|
+
* `node:crypto` rather than `Bun.hash`: the jx bin runs under Node, where the Bun global does
|
|
187
|
+
* not exist, and this line is reached by any page registering npm `$elements`.
|
|
188
|
+
*/
|
|
189
|
+
const key = createHash("sha1").update(sorted.join("\u0000")).digest("hex").slice(0, 12);
|
|
184
190
|
const assetPath = `/assets/elements-${key}.js`;
|
|
185
191
|
elementBundles.set(assetPath, sorted);
|
|
186
192
|
return assetPath;
|
|
@@ -1739,15 +1745,8 @@ function evaluateMapTemplate(str: string, scope: Record<string, unknown>) {
|
|
|
1739
1745
|
try {
|
|
1740
1746
|
const item = (scope.$map as Record<string, unknown>)?.item;
|
|
1741
1747
|
const index = (scope.$map as Record<string, unknown>)?.index;
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
const fn = new Function(
|
|
1745
|
-
"state",
|
|
1746
|
-
"$map",
|
|
1747
|
-
"item",
|
|
1748
|
-
"index",
|
|
1749
|
-
`return (${singleExprMatch[1]})`,
|
|
1750
|
-
) as (
|
|
1748
|
+
if (isSingleExpression(str)) {
|
|
1749
|
+
const fn = new Function("state", "$map", "item", "index", `return (${str.slice(2, -1)})`) as (
|
|
1751
1750
|
state: Record<string, unknown>,
|
|
1752
1751
|
$map: unknown,
|
|
1753
1752
|
item: unknown,
|
|
@@ -2038,6 +2037,23 @@ function expandComponents(
|
|
|
2038
2037
|
}
|
|
2039
2038
|
}
|
|
2040
2039
|
|
|
2040
|
+
/*
|
|
2041
|
+
* A non-static instance discards its prerendered markup on upgrade and re-renders, so the
|
|
2042
|
+
* props have to survive as data or the element comes back with its state defaults — correct
|
|
2043
|
+
* content painting first and then being replaced by the wrong content. compiler.md §5.2 gives
|
|
2044
|
+
* connectedCallback three prop sources and this is the first of them; the compiler read it but
|
|
2045
|
+
* never wrote it, and lifting `props.*` into $props above removed the other channel too.
|
|
2046
|
+
*
|
|
2047
|
+
* JSON rather than re-emitted `props.*` attributes because those are strings by spec: a number
|
|
2048
|
+
* or an array would arrive stringified, and a key that is not lowercase would be dropped by
|
|
2049
|
+
* attribute-name folding.
|
|
2050
|
+
*/
|
|
2051
|
+
if (!isStatic && node.$props && Object.keys(node.$props).length > 0) {
|
|
2052
|
+
node.attributes = {
|
|
2053
|
+
...node.attributes,
|
|
2054
|
+
"data-jx-props": JSON.stringify(node.$props),
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2041
2057
|
delete node.$props;
|
|
2042
2058
|
|
|
2043
2059
|
if (isStatic) {
|
|
@@ -593,7 +593,15 @@ export function emitElementModule(
|
|
|
593
593
|
// Declarations emitted inside `template()`, above the `return` — inside it, so they are rebuilt
|
|
594
594
|
// Per render and read the same `s` the template does.
|
|
595
595
|
const hoistScope = createHoistScope();
|
|
596
|
-
|
|
596
|
+
/*
|
|
597
|
+
* A component whose content is root-level `textContent` has no `children`, and emitLitChildren
|
|
598
|
+
* returns "" for that — so the element rendered nothing, matching a prerender that dropped the
|
|
599
|
+
* same shape. spec.md §17.2 recommends `textContent` whenever every child is a bare string.
|
|
600
|
+
*/
|
|
601
|
+
const templateBody =
|
|
602
|
+
!Array.isArray(doc.children) && doc.textContent !== undefined
|
|
603
|
+
? ` ${toLitTextContent(doc.textContent)}`
|
|
604
|
+
: emitLitChildren(doc.children, doc.style, " ", false, false, hoistScope);
|
|
597
605
|
|
|
598
606
|
lines.push(
|
|
599
607
|
// Template method
|