@jxsuite/compiler 2.0.7 → 3.0.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/README.md +8 -8
- package/dist/cli.js +193 -190
- package/dist/compiler.js +110 -111
- package/package.json +11 -10
- package/src/shared.ts +99 -31
- package/src/site/bundler.ts +28 -4
- package/src/site/client-runtime.ts +68 -9
- package/src/site/context-injection.ts +30 -128
- package/src/site/i18n.ts +10 -26
- package/src/site/image-optimizer.ts +31 -1
- package/src/site/image-transform.ts +141 -7
- package/src/site/layout-resolver.ts +30 -192
- package/src/site/pages-discovery.ts +9 -49
- package/src/site/site-build.ts +128 -42
- package/src/targets/compile-element.ts +60 -2
- package/src/types.ts +5 -11
- package/src/site/head-merger.ts +0 -251
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";
|
|
@@ -49,7 +50,7 @@ import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
|
49
50
|
import type { FormatEntry, FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
50
51
|
import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
51
52
|
import { resolveLayout } from "./layout-resolver.ts";
|
|
52
|
-
import { mergeHead, renderHead } from "
|
|
53
|
+
import { mergeHead, renderHead } from "@jxsuite/site/head-merger";
|
|
53
54
|
import { unregisteredHeadRelations } from "./link-relations.ts";
|
|
54
55
|
import { injectContext } from "./context-injection.ts";
|
|
55
56
|
import { compile, compileServer, compileSiteServer } from "../compiler.ts";
|
|
@@ -61,8 +62,10 @@ import {
|
|
|
61
62
|
collectServerEntries,
|
|
62
63
|
collectStyles,
|
|
63
64
|
colorSchemePrePaintScript,
|
|
65
|
+
escapeStyleText,
|
|
64
66
|
evaluateStaticTemplate,
|
|
65
67
|
isComponentFullyStatic,
|
|
68
|
+
isSingleExpression,
|
|
66
69
|
isTemplateString,
|
|
67
70
|
preRenderComponentHtml,
|
|
68
71
|
pureSchemeOf,
|
|
@@ -97,7 +100,8 @@ import {
|
|
|
97
100
|
} from "./i18n.ts";
|
|
98
101
|
import type { LocaleAlternate, PageLocaleContext } from "./i18n.ts";
|
|
99
102
|
import {
|
|
100
|
-
|
|
103
|
+
importMapAssets,
|
|
104
|
+
importMapAssetsInHtml,
|
|
101
105
|
renderImportMap,
|
|
102
106
|
resolveClientRuntime,
|
|
103
107
|
writeClientRuntime,
|
|
@@ -158,6 +162,13 @@ export async function buildSite(
|
|
|
158
162
|
const pagesDir = resolve(projectRoot, "pages");
|
|
159
163
|
const publicDir = resolve(projectRoot, "public");
|
|
160
164
|
const trailingSlash = projectConfig.build.trailingSlash ?? "always";
|
|
165
|
+
/*
|
|
166
|
+
* Browser bundles are minified unless the project opts out. Off, the six scripts jxsuite.com
|
|
167
|
+
* loads weigh 155 kB of source a phone has to parse before it can paint; on, 65 kB. The opt-out
|
|
168
|
+
* exists for two real cases: reading a deployed stack trace, and diffing `dist/` across bundler
|
|
169
|
+
* backends, which agree on semantics but not on minified bytes.
|
|
170
|
+
*/
|
|
171
|
+
const minify = projectConfig.build.minify !== false;
|
|
161
172
|
|
|
162
173
|
// Client sidecar bundles: bundleable Function-def `$src` specifiers (and lowered-def `$bundle`
|
|
163
174
|
// Hints) are rewritten to their /assets/ URL during compilation, registered here, and bundled
|
|
@@ -180,7 +191,11 @@ export async function buildSite(
|
|
|
180
191
|
const elementBundles = new Map<string, string[]>();
|
|
181
192
|
const registerElementBundle = (specifiers: string[]): string => {
|
|
182
193
|
const sorted = specifiers.toSorted();
|
|
183
|
-
|
|
194
|
+
/*
|
|
195
|
+
* `node:crypto` rather than `Bun.hash`: the jx bin runs under Node, where the Bun global does
|
|
196
|
+
* not exist, and this line is reached by any page registering npm `$elements`.
|
|
197
|
+
*/
|
|
198
|
+
const key = createHash("sha1").update(sorted.join("\u0000")).digest("hex").slice(0, 12);
|
|
184
199
|
const assetPath = `/assets/elements-${key}.js`;
|
|
185
200
|
elementBundles.set(assetPath, sorted);
|
|
186
201
|
return assetPath;
|
|
@@ -195,6 +210,11 @@ export async function buildSite(
|
|
|
195
210
|
*/
|
|
196
211
|
const clientRuntime = resolveClientRuntime();
|
|
197
212
|
const runtimeImports = clientRuntime.imports;
|
|
213
|
+
/*
|
|
214
|
+
* What step 6d bundles, scanned back out of each finished page rather than recorded wherever a
|
|
215
|
+
* map is emitted. Several places emit one and only `injectComponentScripts` was recording, so a
|
|
216
|
+
* page whose interactivity came from anything else shipped a map naming files no build wrote.
|
|
217
|
+
*/
|
|
198
218
|
const runtimeAssetsUsed = new Set<string>();
|
|
199
219
|
/*
|
|
200
220
|
* Scanned from every finished page, so the hashes name the exact bytes shipped rather than what
|
|
@@ -674,10 +694,18 @@ export async function buildSite(
|
|
|
674
694
|
staticTags,
|
|
675
695
|
result.files.map((f: { content: string }) => f.content).join("\n"),
|
|
676
696
|
runtimeImports,
|
|
677
|
-
runtimeAssetsUsed,
|
|
678
697
|
);
|
|
679
698
|
}
|
|
680
699
|
|
|
700
|
+
/*
|
|
701
|
+
* Whatever import map this page ended up with, step 6d owes it the files it names. Read from
|
|
702
|
+
* the finished HTML so the answer does not depend on which of the emission paths produced
|
|
703
|
+
* the map — the one that reported and the one that shipped were different sets (issue #227).
|
|
704
|
+
*/
|
|
705
|
+
for (const assetPath of importMapAssetsInHtml(result.html)) {
|
|
706
|
+
runtimeAssetsUsed.add(assetPath);
|
|
707
|
+
}
|
|
708
|
+
|
|
681
709
|
// Mounted assets this page actually references — scanned from the finished HTML so a
|
|
682
710
|
// Reference is caught wherever it came from (markdown image, hand-authored page, $head).
|
|
683
711
|
collectAssetRefs(result.html, assetMounts, assetRefs);
|
|
@@ -856,6 +884,8 @@ export async function buildSite(
|
|
|
856
884
|
const runtime = await writeClientRuntime(
|
|
857
885
|
{ ...clientRuntime, assetPaths: [...runtimeAssetsUsed] },
|
|
858
886
|
outDir,
|
|
887
|
+
undefined,
|
|
888
|
+
minify,
|
|
859
889
|
);
|
|
860
890
|
fileCount += runtime.written;
|
|
861
891
|
for (const error of runtime.errors) {
|
|
@@ -883,7 +913,7 @@ export async function buildSite(
|
|
|
883
913
|
mkdirSync(dirname(outfile), { recursive: true });
|
|
884
914
|
await bundleEntry(
|
|
885
915
|
{ entryPath: bundle.entryPath, outfile },
|
|
886
|
-
{ external: CLIENT_EXTERNALS, target: "browser" },
|
|
916
|
+
{ external: CLIENT_EXTERNALS, minify, target: "browser" },
|
|
887
917
|
);
|
|
888
918
|
fileCount += 1;
|
|
889
919
|
log(` ${bundle.specifier} → ${assetPath}`);
|
|
@@ -908,7 +938,7 @@ export async function buildSite(
|
|
|
908
938
|
await bundleSource(
|
|
909
939
|
`${specifiers.map((sp) => `import ${JSON.stringify(sp)};`).join("\n")}\n`,
|
|
910
940
|
{ outfile, resolveDir: projectRoot },
|
|
911
|
-
{ target: "browser" },
|
|
941
|
+
{ minify, target: "browser" },
|
|
912
942
|
);
|
|
913
943
|
fileCount += 1;
|
|
914
944
|
log(` ${specifiers.length} element(s) → ${assetPath}`);
|
|
@@ -927,7 +957,7 @@ export async function buildSite(
|
|
|
927
957
|
* discovered from their contents rather than declared anywhere (site-architecture.md §8.7).
|
|
928
958
|
*/
|
|
929
959
|
if (runtimeAssetsUsed.size > 0) {
|
|
930
|
-
const subpaths = await writeRuntimeSubpaths(outDir);
|
|
960
|
+
const subpaths = await writeRuntimeSubpaths(outDir, undefined, minify);
|
|
931
961
|
if (subpaths.written > 0) {
|
|
932
962
|
log(`Bundling ${subpaths.written} runtime subpath module(s)...`);
|
|
933
963
|
}
|
|
@@ -1089,12 +1119,36 @@ export async function buildSite(
|
|
|
1089
1119
|
if (rules.length > 0) {
|
|
1090
1120
|
log("Writing response headers...");
|
|
1091
1121
|
fileCount += writeHeaders(outDir, rules);
|
|
1092
|
-
const { adapter } = projectConfig.build;
|
|
1122
|
+
const { adapter, deploy } = projectConfig.build;
|
|
1093
1123
|
if (adapter === "node" || adapter === "bun") {
|
|
1094
1124
|
console.warn(
|
|
1095
1125
|
`The "${adapter}" adapter serves no static assets, so dist/_headers is documentation ` +
|
|
1096
1126
|
`rather than configuration — apply these headers at the reverse proxy in front of it.`,
|
|
1097
1127
|
);
|
|
1128
|
+
} else if (adapter === undefined && deploy === undefined) {
|
|
1129
|
+
/*
|
|
1130
|
+
* `_headers` is a Cloudflare/Netlify convention, not a web standard, and the build has no
|
|
1131
|
+
* way to know where a plain static `dist/` is going. Most static hosts — GitHub Pages among
|
|
1132
|
+
* them — ignore the file entirely and serve their own `Cache-Control`, which is how a site
|
|
1133
|
+
* ends up shipping a year-long `immutable` rule for its content-addressed images and
|
|
1134
|
+
* getting ten minutes.
|
|
1135
|
+
*
|
|
1136
|
+
* A `public/CNAME` is GitHub Pages' custom-domain marker and nothing else's, so it is a
|
|
1137
|
+
* specific enough signal to say so out loud rather than warn every static build.
|
|
1138
|
+
*/
|
|
1139
|
+
if (existsSync(resolve(publicDir, "CNAME"))) {
|
|
1140
|
+
console.warn(
|
|
1141
|
+
"GitHub Pages ignores dist/_headers and serves its own Cache-Control: max-age=600, " +
|
|
1142
|
+
"so the generated caching and security rules will not take effect. Deploy behind a " +
|
|
1143
|
+
"host that reads _headers (Cloudflare Pages, Cloudflare Workers assets, Netlify) " +
|
|
1144
|
+
"to apply them.",
|
|
1145
|
+
);
|
|
1146
|
+
} else {
|
|
1147
|
+
log(
|
|
1148
|
+
" note: _headers is read by Cloudflare Pages, Cloudflare Workers assets and Netlify; " +
|
|
1149
|
+
"other static hosts ignore it and serve their own Cache-Control.",
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1098
1152
|
}
|
|
1099
1153
|
}
|
|
1100
1154
|
fileCount += writeNoJekyll(outDir);
|
|
@@ -1165,7 +1219,7 @@ async function compilePage(
|
|
|
1165
1219
|
const pageDoc = await readPageDocument(route.sourcePath as string, formatRegistry);
|
|
1166
1220
|
|
|
1167
1221
|
// Resolve layout (wraps page in layout with slot distribution)
|
|
1168
|
-
const layoutDoc = resolveLayout(pageDoc, projectConfig, projectRoot);
|
|
1222
|
+
const layoutDoc = await resolveLayout(pageDoc, projectConfig, projectRoot);
|
|
1169
1223
|
|
|
1170
1224
|
// Extract head arrays before they get lost in the merge
|
|
1171
1225
|
const pageHead = (pageDoc.$head ?? layoutDoc._pageHead ?? []) as JxHeadEntry[];
|
|
@@ -1387,7 +1441,7 @@ async function compilePage(
|
|
|
1387
1441
|
if (slotCss.rules.length > 0) {
|
|
1388
1442
|
result.html = result.html.replace(
|
|
1389
1443
|
"</head>",
|
|
1390
|
-
`<style>\n${slotCss.rules.join("\n")}\n</style>\n</head>`,
|
|
1444
|
+
`<style>\n${escapeStyleText(slotCss.rules.join("\n"))}\n</style>\n</head>`,
|
|
1391
1445
|
);
|
|
1392
1446
|
}
|
|
1393
1447
|
|
|
@@ -1739,15 +1793,8 @@ function evaluateMapTemplate(str: string, scope: Record<string, unknown>) {
|
|
|
1739
1793
|
try {
|
|
1740
1794
|
const item = (scope.$map as Record<string, unknown>)?.item;
|
|
1741
1795
|
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 (
|
|
1796
|
+
if (isSingleExpression(str)) {
|
|
1797
|
+
const fn = new Function("state", "$map", "item", "index", `return (${str.slice(2, -1)})`) as (
|
|
1751
1798
|
state: Record<string, unknown>,
|
|
1752
1799
|
$map: unknown,
|
|
1753
1800
|
item: unknown,
|
|
@@ -2038,6 +2085,23 @@ function expandComponents(
|
|
|
2038
2085
|
}
|
|
2039
2086
|
}
|
|
2040
2087
|
|
|
2088
|
+
/*
|
|
2089
|
+
* A non-static instance discards its prerendered markup on upgrade and re-renders, so the
|
|
2090
|
+
* props have to survive as data or the element comes back with its state defaults — correct
|
|
2091
|
+
* content painting first and then being replaced by the wrong content. compiler.md §5.2 gives
|
|
2092
|
+
* connectedCallback three prop sources and this is the first of them; the compiler read it but
|
|
2093
|
+
* never wrote it, and lifting `props.*` into $props above removed the other channel too.
|
|
2094
|
+
*
|
|
2095
|
+
* JSON rather than re-emitted `props.*` attributes because those are strings by spec: a number
|
|
2096
|
+
* or an array would arrive stringified, and a key that is not lowercase would be dropped by
|
|
2097
|
+
* attribute-name folding.
|
|
2098
|
+
*/
|
|
2099
|
+
if (!isStatic && node.$props && Object.keys(node.$props).length > 0) {
|
|
2100
|
+
node.attributes = {
|
|
2101
|
+
...node.attributes,
|
|
2102
|
+
"data-jx-props": JSON.stringify(node.$props),
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2041
2105
|
delete node.$props;
|
|
2042
2106
|
|
|
2043
2107
|
if (isStatic) {
|
|
@@ -2058,7 +2122,6 @@ function expandComponents(
|
|
|
2058
2122
|
* @param {Set<string>} [staticTags] - Tags where ALL instances are fully static (skip JS)
|
|
2059
2123
|
* @param {string} [islandSource] - Concatenated island modules emitted for this page
|
|
2060
2124
|
* @param {Record<string, string>} [runtimeImports] - The import map's `imports` object
|
|
2061
|
-
* @param {Set<string>} [runtimeAssetsUsed] - Collects the asset paths an emitted import map names
|
|
2062
2125
|
* @returns {string}
|
|
2063
2126
|
*/
|
|
2064
2127
|
function injectComponentScripts(
|
|
@@ -2068,7 +2131,6 @@ function injectComponentScripts(
|
|
|
2068
2131
|
staticTags = new Set<string>(),
|
|
2069
2132
|
islandSource = "",
|
|
2070
2133
|
runtimeImports: Record<string, string> = {},
|
|
2071
|
-
runtimeAssetsUsed = new Set<string>(),
|
|
2072
2134
|
) {
|
|
2073
2135
|
// A component reaches a page two ways: as a literal tag in the static HTML, or referenced only
|
|
2074
2136
|
// From inside an island's client template — in which case the tag exists solely in the island's
|
|
@@ -2081,15 +2143,31 @@ function injectComponentScripts(
|
|
|
2081
2143
|
return html;
|
|
2082
2144
|
}
|
|
2083
2145
|
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2146
|
+
/*
|
|
2147
|
+
* Component CSS is INLINED, not linked.
|
|
2148
|
+
*
|
|
2149
|
+
* A `<link rel="stylesheet">` per component tag is a render-blocking request per component: on
|
|
2150
|
+
* jxsuite.com's home page, 11 of them totalling 22 kB — the whole of its critical request chain,
|
|
2151
|
+
* for sheets averaging 2 kB each. The page already carries its project and page CSS inline
|
|
2152
|
+
* (`buildStyleBlock`), so this is the same delivery, applied consistently.
|
|
2153
|
+
*
|
|
2154
|
+
* The trade is real and deliberate: these bytes now repeat on every page instead of caching
|
|
2155
|
+
* across navigations. They are small, they gzip against the markup around them, and a round trip
|
|
2156
|
+
* on a phone costs more than the bytes do. Components in shadow mode are untouched — their sheet
|
|
2157
|
+
* is linked from inside the declarative shadow root and never enters `cssMap`.
|
|
2158
|
+
*
|
|
2159
|
+
* Emitted last in <head>, exactly where the links were, so the cascade is unchanged.
|
|
2160
|
+
*/
|
|
2161
|
+
const componentCss = usedTags
|
|
2162
|
+
.map((tag: string) => cssMap.get(tag))
|
|
2163
|
+
.filter((css): css is string => css !== undefined && css !== "")
|
|
2164
|
+
.join("\n");
|
|
2165
|
+
const styleBlock =
|
|
2166
|
+
componentCss === "" ? "" : `<style>\n${escapeStyleText(componentCss)}\n </style>`;
|
|
2167
|
+
const intoHead = (fragments: string[]) => {
|
|
2168
|
+
const block = fragments.filter(Boolean).join("\n ");
|
|
2169
|
+
return block === "" ? html : html.replace("</head>", ` ${block}\n</head>`);
|
|
2170
|
+
};
|
|
2093
2171
|
|
|
2094
2172
|
// Only inject JS for components that have non-static instances. An island-rendered instance is
|
|
2095
2173
|
// Never prerendered, so it needs its module even when the component itself is fully static —
|
|
@@ -2097,22 +2175,30 @@ function injectComponentScripts(
|
|
|
2097
2175
|
// The static HTML: those instances are prerendered, but the ones the island creates are not.
|
|
2098
2176
|
const jsTags = usedTags.filter((tag: string) => inIsland(tag) || !staticTags.has(tag));
|
|
2099
2177
|
if (jsTags.length === 0) {
|
|
2100
|
-
return
|
|
2178
|
+
return intoHead([styleBlock]);
|
|
2101
2179
|
}
|
|
2102
2180
|
|
|
2103
2181
|
// Build import map (needed for @vue/reactivity and lit-html)
|
|
2104
2182
|
const importMap = renderImportMap(runtimeImports);
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2183
|
+
/*
|
|
2184
|
+
* The runtime modules THIS page needs — the hints are per page, so they are read from this
|
|
2185
|
+
* page's map rather than from the build-wide set, which would have every page hint at every
|
|
2186
|
+
* other page's runtime.
|
|
2187
|
+
*/
|
|
2188
|
+
const pageRuntimeAssets = importMapAssets(runtimeImports);
|
|
2189
|
+
|
|
2190
|
+
/*
|
|
2191
|
+
* An import map declares where a bare specifier lives; it does not tell the browser to go get it.
|
|
2192
|
+
* Without these hints `/assets/vue-reactivity.js` is discovered only after a component module has
|
|
2193
|
+
* been fetched AND parsed — a three-deep request chain on the critical path, on a connection
|
|
2194
|
+
* where each hop is a round trip. The component modules are hinted too so the whole set starts
|
|
2195
|
+
* downloading while the HTML is still being parsed.
|
|
2196
|
+
*/
|
|
2197
|
+
const preloads = [...pageRuntimeAssets, ...jsTags.map((tag: string) => `/components/${tag}.js`)]
|
|
2198
|
+
.map((href) => `<link rel="modulepreload" href="${href}">`)
|
|
2199
|
+
.join("\n ");
|
|
2200
|
+
|
|
2201
|
+
const result = intoHead([preloads, styleBlock]);
|
|
2116
2202
|
|
|
2117
2203
|
const moduleScripts = jsTags
|
|
2118
2204
|
.map((tag: string) => `<script type="module" src="/components/${tag}.js"></script>`)
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
escapeHtml,
|
|
18
18
|
isMutating,
|
|
19
19
|
isSchemaOnly,
|
|
20
|
+
srcExportName,
|
|
20
21
|
srcImportBinding,
|
|
21
22
|
tagNameToClassName,
|
|
22
23
|
} from "../shared.ts";
|
|
@@ -351,11 +352,36 @@ export function emitElementModule(
|
|
|
351
352
|
|
|
352
353
|
// Collect $src imports from state entries before emitting other imports
|
|
353
354
|
const srcImportMap = new Map<string, string[]>();
|
|
355
|
+
/** `$lazy` `$src` modules → the (export, local binding) pairs each one supplies. */
|
|
356
|
+
const lazySrcMap = new Map<string, { binding: string; exportName: string }[]>();
|
|
354
357
|
const defs = doc.state ?? {};
|
|
358
|
+
const callableRefs = collectCallableRefs(doc);
|
|
355
359
|
for (const [key, def] of Object.entries(defs)) {
|
|
356
360
|
const d = def as JxMutableNode;
|
|
357
361
|
if (d && typeof d === "object" && !Array.isArray(d) && d.$prototype === "Function" && d.$src) {
|
|
358
362
|
const srcPath = d.$src;
|
|
363
|
+
if (d.$lazy === true) {
|
|
364
|
+
/*
|
|
365
|
+
* A computed reads its value synchronously, and a lazy binding can only ever hand back a
|
|
366
|
+
* promise. Silently producing `computed(() => Promise)` would render "[object Promise]" in
|
|
367
|
+
* whatever the template bound it to, so this is a build error rather than a surprise.
|
|
368
|
+
*/
|
|
369
|
+
if (!callableRefs.has(key) && !LIFECYCLE_KEYS.has(key)) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`"${key}" declares "$lazy": true but is used as a computed value, not called. ` +
|
|
372
|
+
`A lazily loaded function resolves to a promise; a computed must resolve now. ` +
|
|
373
|
+
`Remove "$lazy", or call ${key}() from a handler instead of binding its value.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (!lazySrcMap.has(srcPath)) {
|
|
377
|
+
lazySrcMap.set(srcPath, []);
|
|
378
|
+
}
|
|
379
|
+
(lazySrcMap.get(srcPath) as { binding: string; exportName: string }[]).push({
|
|
380
|
+
binding: key,
|
|
381
|
+
exportName: srcExportName(key, d),
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
359
385
|
if (!srcImportMap.has(srcPath)) {
|
|
360
386
|
srcImportMap.set(srcPath, []);
|
|
361
387
|
}
|
|
@@ -380,6 +406,31 @@ export function emitElementModule(
|
|
|
380
406
|
lines.push(
|
|
381
407
|
`import { reactive, computed, effect, stop } from '@vue/reactivity';`,
|
|
382
408
|
`import { render, html } from 'lit-html';`,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
/*
|
|
412
|
+
* `$lazy` swaps the static import for a memoized dynamic one. A static import is a download, a
|
|
413
|
+
* parse and an evaluate on every page that renders the component, whether or not anybody calls
|
|
414
|
+
* the function: jxsuite.com's search client is 70 kB of MiniSearch fetched by every visitor, most
|
|
415
|
+
* of whom never open search. The local binding keeps its name so every call site is unchanged;
|
|
416
|
+
* what changes is that it now returns a promise.
|
|
417
|
+
*/
|
|
418
|
+
let lazyIndex = 0;
|
|
419
|
+
for (const [srcPath, entries] of lazySrcMap) {
|
|
420
|
+
const importPath = rewriteSrc ? rewriteSrc(srcPath) : srcPath;
|
|
421
|
+
const mod = `_jxLazy${lazyIndex}`;
|
|
422
|
+
lazyIndex += 1;
|
|
423
|
+
lines.push(
|
|
424
|
+
`let ${mod};`,
|
|
425
|
+
`const ${mod}Load = () => (${mod} ??= import('${importPath}'));`,
|
|
426
|
+
...entries.map(
|
|
427
|
+
({ binding, exportName }) =>
|
|
428
|
+
`const ${binding} = (...args) => ${mod}Load().then((m) => m.${exportName}(...args));`,
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
lines.push(
|
|
383
434
|
"",
|
|
384
435
|
`class ${className} extends HTMLElement {`,
|
|
385
436
|
// One registry for every effect the element creates — render, dynamic host styles, Request
|
|
@@ -401,7 +452,6 @@ export function emitElementModule(
|
|
|
401
452
|
const formulaEntries: [string, JxExpressionDef][] = [];
|
|
402
453
|
/** `"${…}"` state entries — computed, exactly as the runtime treats them. */
|
|
403
454
|
const templateEntries: [string, string][] = [];
|
|
404
|
-
const callableRefs = collectCallableRefs(doc);
|
|
405
455
|
for (const [key, def] of Object.entries(defs)) {
|
|
406
456
|
const d = def as JxMutableNode;
|
|
407
457
|
if (isExpressionDef(d)) {
|
|
@@ -593,7 +643,15 @@ export function emitElementModule(
|
|
|
593
643
|
// Declarations emitted inside `template()`, above the `return` — inside it, so they are rebuilt
|
|
594
644
|
// Per render and read the same `s` the template does.
|
|
595
645
|
const hoistScope = createHoistScope();
|
|
596
|
-
|
|
646
|
+
/*
|
|
647
|
+
* A component whose content is root-level `textContent` has no `children`, and emitLitChildren
|
|
648
|
+
* returns "" for that — so the element rendered nothing, matching a prerender that dropped the
|
|
649
|
+
* same shape. spec.md §17.2 recommends `textContent` whenever every child is a bare string.
|
|
650
|
+
*/
|
|
651
|
+
const templateBody =
|
|
652
|
+
!Array.isArray(doc.children) && doc.textContent !== undefined
|
|
653
|
+
? ` ${toLitTextContent(doc.textContent)}`
|
|
654
|
+
: emitLitChildren(doc.children, doc.style, " ", false, false, hoistScope);
|
|
597
655
|
|
|
598
656
|
lines.push(
|
|
599
657
|
// Template method
|
package/src/types.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
url?: string;
|
|
7
|
-
siteUrl?: string;
|
|
8
|
-
pageUrl?: string;
|
|
9
|
-
/** `rel="alternate"` links for this page's translations (site-architecture.md §13.5). */
|
|
10
|
-
alternates?: readonly { hreflang: string; href: string }[];
|
|
11
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `HeadMergeContext` moved to `@jxsuite/site/head-merger` with the merger itself — a browser bundle
|
|
3
|
+
* and a Worker both assemble the same `<head>`, and neither can import this package.
|
|
4
|
+
*/
|
|
5
|
+
export type { HeadMergeContext } from "@jxsuite/site/head-merger";
|
|
12
6
|
|
|
13
7
|
export interface SiteRoute {
|
|
14
8
|
urlPattern: string;
|