@jxsuite/compiler 1.1.0 → 1.2.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/dist/cli.js +255 -186
- package/dist/compiler.js +111 -109
- package/package.json +7 -6
- package/src/compiler.ts +18 -2
- package/src/shared.ts +167 -108
- package/src/site/bundler.ts +187 -0
- package/src/site/prototype-resolver.ts +15 -0
- package/src/site/site-build.ts +147 -13
- package/src/targets/compile-client.ts +17 -3
- package/src/targets/compile-element.ts +28 -3
- package/src/targets/compile-static.ts +17 -2
package/src/site/site-build.ts
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
copyFileSync,
|
|
13
12
|
cpSync,
|
|
14
13
|
existsSync,
|
|
15
14
|
mkdirSync,
|
|
@@ -19,8 +18,16 @@ import {
|
|
|
19
18
|
statSync,
|
|
20
19
|
writeFileSync,
|
|
21
20
|
} from "node:fs";
|
|
22
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
21
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
23
22
|
import { isMappedArray, isRef } from "@jxsuite/schema/guards";
|
|
23
|
+
import { isNpmSpecifier, sidecarAssetPath } from "@jxsuite/schema/asset-paths";
|
|
24
|
+
import {
|
|
25
|
+
CLIENT_EXTERNALS,
|
|
26
|
+
bundleEntry,
|
|
27
|
+
bundleWorkerSource,
|
|
28
|
+
isBundleableSrc,
|
|
29
|
+
resolveSidecarEntry,
|
|
30
|
+
} from "./bundler.ts";
|
|
24
31
|
import { loadProjectConfig } from "./site-loader.ts";
|
|
25
32
|
import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
|
|
26
33
|
import { buildProjectExtensionRegistry } from "./format-host.ts";
|
|
@@ -40,10 +47,12 @@ import {
|
|
|
40
47
|
buildInitialScope,
|
|
41
48
|
collectServerEntries,
|
|
42
49
|
collectStyles,
|
|
50
|
+
colorSchemePrePaintScript,
|
|
43
51
|
evaluateStaticTemplate,
|
|
44
52
|
isComponentFullyStatic,
|
|
45
53
|
isTemplateString,
|
|
46
54
|
preRenderComponentHtml,
|
|
55
|
+
pureSchemeOf,
|
|
47
56
|
renderStaticNode,
|
|
48
57
|
resolveRefValue,
|
|
49
58
|
resolveStaticValue,
|
|
@@ -106,6 +115,43 @@ export async function buildSite(
|
|
|
106
115
|
const publicDir = resolve(projectRoot, "public");
|
|
107
116
|
const trailingSlash = projectConfig.build.trailingSlash ?? "always";
|
|
108
117
|
|
|
118
|
+
// Client sidecar bundles: bundleable Function-def `$src` specifiers (and lowered-def `$bundle`
|
|
119
|
+
// Hints) are rewritten to their /assets/ URL during compilation, registered here, and bundled
|
|
120
|
+
// In step 6d (spec.md §12). Keyed by asset path so duplicate specifiers bundle once.
|
|
121
|
+
const sidecarBundles = new Map<string, { entryPath: string; specifier: string }>();
|
|
122
|
+
const rewriteSidecarSrc = (specifier: string, docDir: string | null): string => {
|
|
123
|
+
if (!isBundleableSrc(specifier)) {
|
|
124
|
+
return specifier;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const entryPath = resolveSidecarEntry(specifier, docDir ?? projectRoot, projectRoot);
|
|
128
|
+
// A declared-but-absent relative sidecar keeps its verbatim import (the historical
|
|
129
|
+
// Behavior) with a warning — the doc may only ever run interpreted. Unresolvable npm
|
|
130
|
+
// Specifiers stay hard errors: a missing dependency should fail the build.
|
|
131
|
+
if (!isNpmSpecifier(specifier) && !existsSync(entryPath)) {
|
|
132
|
+
console.warn(`Sidecar "${specifier}" not found at ${entryPath} — emitting unbundled`);
|
|
133
|
+
return specifier;
|
|
134
|
+
}
|
|
135
|
+
// Relative specifiers key on their project-relative resolved path so `./x.js` declared in
|
|
136
|
+
// Two directories cannot collide on one slug; npm specifiers are location-independent.
|
|
137
|
+
const assetKey = isNpmSpecifier(specifier)
|
|
138
|
+
? specifier
|
|
139
|
+
: `./${relative(projectRoot, entryPath)}`;
|
|
140
|
+
const assetPath = sidecarAssetPath(assetKey);
|
|
141
|
+
const existing = sidecarBundles.get(assetPath);
|
|
142
|
+
if (existing && existing.entryPath !== entryPath) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`"${specifier}" and "${existing.specifier}" both bundle to ${assetPath} — rename one`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
sidecarBundles.set(assetPath, { entryPath, specifier });
|
|
148
|
+
return assetPath;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
errors.push(`Sidecar "${specifier}": ${(error as Error).message}`);
|
|
151
|
+
return specifier;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
109
155
|
// ── 1b. Build the extension registry from project.json#/extensions ─────
|
|
110
156
|
const registry = await buildProjectExtensionRegistry(projectRoot, projectConfig);
|
|
111
157
|
const formatRegistry = registry.formats;
|
|
@@ -194,6 +240,7 @@ export async function buildSite(
|
|
|
194
240
|
const result = await compileElement(componentPath, {
|
|
195
241
|
...(projectConfig.$media ? { $media: projectConfig.$media } : {}),
|
|
196
242
|
formats: formatRegistry,
|
|
243
|
+
rewriteSrc: rewriteSidecarSrc,
|
|
197
244
|
});
|
|
198
245
|
for (const f of result.files) {
|
|
199
246
|
// Reduce the emitted path (an absolute source path with .json swapped to .js) to a bare
|
|
@@ -277,6 +324,7 @@ export async function buildSite(
|
|
|
277
324
|
imageMetaCache,
|
|
278
325
|
formatRegistry,
|
|
279
326
|
registry,
|
|
327
|
+
rewriteSidecarSrc,
|
|
280
328
|
);
|
|
281
329
|
|
|
282
330
|
// Determine which component tags are fully static (for script omission)
|
|
@@ -412,10 +460,23 @@ export async function buildSite(
|
|
|
412
460
|
: compileSiteServer([...deduped.values()], { adapter, connectors, mounts });
|
|
413
461
|
|
|
414
462
|
if (workerSource) {
|
|
463
|
+
// Bundle the worker self-contained for the adapter's runtime (compiler.md §12): mount
|
|
464
|
+
// Modules, connectors, hono, and user server functions are inlined, so dist deploys
|
|
465
|
+
// Without node_modules or deploy-time bundling.
|
|
415
466
|
const workerName = adapter === "cloudflare-pages" ? "_worker.js" : "worker.js";
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
467
|
+
try {
|
|
468
|
+
await bundleWorkerSource(workerSource, {
|
|
469
|
+
adapter,
|
|
470
|
+
outfile: resolve(outDir, workerName),
|
|
471
|
+
projectRoot,
|
|
472
|
+
});
|
|
473
|
+
fileCount += 1;
|
|
474
|
+
log(` Generated dist/${workerName} (bundled, ${deduped.size} server function(s))`);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
const msg = `Error bundling dist/${workerName}: ${(error as Error).message}`;
|
|
477
|
+
errors.push(msg);
|
|
478
|
+
console.error(msg);
|
|
479
|
+
}
|
|
419
480
|
|
|
420
481
|
if (adapter === "cloudflare-pages") {
|
|
421
482
|
// Only invoke the worker for server routes; everything else stays static.
|
|
@@ -426,17 +487,68 @@ export async function buildSite(
|
|
|
426
487
|
);
|
|
427
488
|
fileCount += 1;
|
|
428
489
|
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── 6d. Bundle client sidecar modules (spec.md §12) ─────────────────────
|
|
494
|
+
if (sidecarBundles.size > 0) {
|
|
495
|
+
log(`Bundling ${sidecarBundles.size} client sidecar module(s)...`);
|
|
496
|
+
for (const [assetPath, bundle] of sidecarBundles) {
|
|
497
|
+
try {
|
|
498
|
+
const outfile = resolve(outDir, assetPath.replace(/^\//, ""));
|
|
499
|
+
mkdirSync(dirname(outfile), { recursive: true });
|
|
500
|
+
await bundleEntry(
|
|
501
|
+
{ entryPath: bundle.entryPath, outfile },
|
|
502
|
+
{ external: CLIENT_EXTERNALS, target: "browser" },
|
|
503
|
+
);
|
|
504
|
+
fileCount += 1;
|
|
505
|
+
log(` ${bundle.specifier} → ${assetPath}`);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
const msg = `Error bundling sidecar "${bundle.specifier}": ${(error as Error).message}`;
|
|
508
|
+
errors.push(msg);
|
|
509
|
+
console.error(msg);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
429
513
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
514
|
+
// ── 6e. Extension-emitted assets (extensions.md §8.4) ───────────────────
|
|
515
|
+
// Section-owner classes with an `emit` capability contribute derived build artifacts
|
|
516
|
+
// (search indexes, feeds, …). The host writes the returned files so extensions stay pure;
|
|
517
|
+
// Paths are outDir-relative and guarded against traversal. Note the `public/` copy (7c)
|
|
518
|
+
// Runs later and can shadow emitted files — same semantics as sitemap.xml.
|
|
519
|
+
for (const entry of registry.emitters()) {
|
|
520
|
+
const sectionKey = entry.project?.key;
|
|
521
|
+
const sectionValue = sectionKey
|
|
522
|
+
? (projectConfig as unknown as Record<string, unknown>)[sectionKey]
|
|
523
|
+
: undefined;
|
|
524
|
+
if (
|
|
525
|
+
sectionKey &&
|
|
526
|
+
(sectionValue == null ||
|
|
527
|
+
(typeof sectionValue === "object" && Object.keys(sectionValue).length === 0))
|
|
528
|
+
) {
|
|
529
|
+
continue; // Section owner with nothing declared — same gating as sections/mounts
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const emitted = (await entry.call("emit", sectionValue ?? null, {
|
|
533
|
+
projectConfig,
|
|
534
|
+
root: projectRoot,
|
|
535
|
+
routes,
|
|
536
|
+
sections,
|
|
537
|
+
})) as { path: string; content: string | Uint8Array }[] | null;
|
|
538
|
+
for (const file of emitted ?? []) {
|
|
539
|
+
const target = resolve(outDir, file.path.replace(/^\//, ""));
|
|
540
|
+
if (!target.startsWith(outDir + sep)) {
|
|
541
|
+
throw new Error(`emit path escapes outDir: ${file.path}`);
|
|
438
542
|
}
|
|
543
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
544
|
+
writeFileSync(target, file.content);
|
|
545
|
+
fileCount += 1;
|
|
546
|
+
log(` ${entry.name} emitted ${file.path}`);
|
|
439
547
|
}
|
|
548
|
+
} catch (error) {
|
|
549
|
+
const msg = `Error in ${entry.name}.emit: ${(error as Error).message}`;
|
|
550
|
+
errors.push(msg);
|
|
551
|
+
console.error(msg);
|
|
440
552
|
}
|
|
441
553
|
}
|
|
442
554
|
|
|
@@ -517,6 +629,7 @@ async function compilePage(
|
|
|
517
629
|
imageMetaCache: ImageMetaCache | null = null,
|
|
518
630
|
formatRegistry?: FormatRegistry,
|
|
519
631
|
registry?: ExtensionRegistry,
|
|
632
|
+
rewriteSidecarSrc?: (specifier: string, docDir: string | null) => string,
|
|
520
633
|
) {
|
|
521
634
|
// Load the raw page document (.json natively, other formats via the registry)
|
|
522
635
|
const pageDoc = await readPageDocument(route.sourcePath as string, formatRegistry);
|
|
@@ -541,6 +654,13 @@ async function compilePage(
|
|
|
541
654
|
config: projectConfig,
|
|
542
655
|
sections,
|
|
543
656
|
...(registry === undefined ? {} : { registry }),
|
|
657
|
+
...(rewriteSidecarSrc === undefined
|
|
658
|
+
? {}
|
|
659
|
+
: {
|
|
660
|
+
registerBundle: (specifier: string) => {
|
|
661
|
+
rewriteSidecarSrc(specifier, null);
|
|
662
|
+
},
|
|
663
|
+
}),
|
|
544
664
|
});
|
|
545
665
|
|
|
546
666
|
// Build scope from resolved state so template strings in title/$head can be evaluated
|
|
@@ -609,6 +729,13 @@ async function compilePage(
|
|
|
609
729
|
layoutDoc.$media = { ...projectConfig.$media, ...layoutDoc.$media };
|
|
610
730
|
}
|
|
611
731
|
|
|
732
|
+
// Declaring a pure color-scheme query opts the page into the forced-scheme contract: restore
|
|
733
|
+
// The visitor's persisted scheme before first paint (spec §9.5). Injected after the
|
|
734
|
+
// Charset/viewport defaults and ahead of every preserved <style> block.
|
|
735
|
+
if (Object.values(layoutDoc.$media ?? {}).some((q) => pureSchemeOf(String(q)) !== null)) {
|
|
736
|
+
mergedHead.splice(2, 0, { tagName: "script", textContent: colorSchemePrePaintScript() });
|
|
737
|
+
}
|
|
738
|
+
|
|
612
739
|
// Transform <img> nodes for responsive image optimization
|
|
613
740
|
if (projectConfig.images?.optimize && (imageCache || imageMetaCache)) {
|
|
614
741
|
await transformImageNodes(
|
|
@@ -623,7 +750,14 @@ async function compilePage(
|
|
|
623
750
|
// Compile the document using the existing compiler
|
|
624
751
|
const result = await compile(layoutDoc, {
|
|
625
752
|
lang: projectConfig.defaults?.lang ?? "en",
|
|
753
|
+
prePaintScheme: false, // Injected via the merged <head> above, not the target template
|
|
626
754
|
projectStyle: projectConfig.style ?? null,
|
|
755
|
+
...(rewriteSidecarSrc === undefined
|
|
756
|
+
? {}
|
|
757
|
+
: {
|
|
758
|
+
rewriteSrc: (specifier: string) =>
|
|
759
|
+
rewriteSidecarSrc(specifier, dirname(route.sourcePath as string)),
|
|
760
|
+
}),
|
|
627
761
|
title,
|
|
628
762
|
});
|
|
629
763
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
DEFAULT_LIT_HTML_SRC,
|
|
17
17
|
DEFAULT_REACTIVITY_SRC,
|
|
18
18
|
buildAttrs,
|
|
19
|
+
colorSchemePrePaintScript,
|
|
19
20
|
compileExpression,
|
|
20
21
|
compileStatements,
|
|
21
22
|
compileStyles,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
isRefObject,
|
|
27
28
|
isSchemaOnly,
|
|
28
29
|
isTemplateString,
|
|
30
|
+
pureSchemeOf,
|
|
29
31
|
resolveStaticValue,
|
|
30
32
|
} from "../shared.ts";
|
|
31
33
|
import {
|
|
@@ -76,6 +78,7 @@ export function compileClient(
|
|
|
76
78
|
litHtmlSrc?: string;
|
|
77
79
|
modulePath?: string;
|
|
78
80
|
projectStyle?: JxStyle | null;
|
|
81
|
+
prePaintScheme?: boolean;
|
|
79
82
|
[key: string]: unknown;
|
|
80
83
|
},
|
|
81
84
|
) {
|
|
@@ -238,6 +241,7 @@ export function compileClient(
|
|
|
238
241
|
srcImportMap,
|
|
239
242
|
counter,
|
|
240
243
|
reactivitySrc,
|
|
244
|
+
opts.rewriteSrc as ((specifier: string) => string) | undefined,
|
|
241
245
|
);
|
|
242
246
|
|
|
243
247
|
// Build importmap entries — always include lit-html since compiled custom elements need it
|
|
@@ -246,13 +250,20 @@ export function compileClient(
|
|
|
246
250
|
` "lit-html": "${litHtmlSrc}"`,
|
|
247
251
|
];
|
|
248
252
|
|
|
253
|
+
// Forced-scheme contract (spec §9.5): restore the persisted scheme before any styles apply
|
|
254
|
+
const prePaint =
|
|
255
|
+
opts.prePaintScheme !== false &&
|
|
256
|
+
Object.values(raw.$media ?? {}).some((q) => pureSchemeOf(String(q)) !== null)
|
|
257
|
+
? `<script>${colorSchemePrePaintScript()}</script>\n `
|
|
258
|
+
: "";
|
|
259
|
+
|
|
249
260
|
const html = `<!DOCTYPE html>
|
|
250
261
|
<html lang="en">
|
|
251
262
|
<head>
|
|
252
263
|
<meta charset="utf-8">
|
|
253
264
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
254
265
|
<title>${escapeHtml(title)}</title>
|
|
255
|
-
<script type="importmap">
|
|
266
|
+
${prePaint}<script type="importmap">
|
|
256
267
|
{
|
|
257
268
|
"imports": {
|
|
258
269
|
${importmapEntries.join(",\n")}
|
|
@@ -757,6 +768,7 @@ function emitClientModule(
|
|
|
757
768
|
needsLit: boolean;
|
|
758
769
|
},
|
|
759
770
|
_reactivitySrc: string,
|
|
771
|
+
rewriteSrc?: (specifier: string) => string,
|
|
760
772
|
) {
|
|
761
773
|
const lines: string[] = [];
|
|
762
774
|
const { needsLit } = counter;
|
|
@@ -776,9 +788,11 @@ function emitClientModule(
|
|
|
776
788
|
lines.push("import { html, render } from 'lit-html';");
|
|
777
789
|
}
|
|
778
790
|
|
|
779
|
-
// $src imports
|
|
791
|
+
// $src imports — bundleable specifiers (npm:…, ./relative) are rewritten to their /assets/
|
|
792
|
+
// Bundle URL; the site build bundles them after all documents compile (spec.md §12).
|
|
780
793
|
for (const [src, names] of srcImportMap) {
|
|
781
|
-
|
|
794
|
+
const importPath = rewriteSrc ? rewriteSrc(src) : src;
|
|
795
|
+
lines.push(`import { ${[...names].join(", ")} } from '${importPath}';`);
|
|
782
796
|
}
|
|
783
797
|
|
|
784
798
|
// State — reactive state
|
|
@@ -41,6 +41,12 @@ import type {
|
|
|
41
41
|
export interface CompileElementOptions {
|
|
42
42
|
/** Resolve an `$elements` ref path to its emitted JS module path. */
|
|
43
43
|
resolveElementPath?: (refPath: string, currentDir: string | null) => string;
|
|
44
|
+
/**
|
|
45
|
+
* Rewrite a Function-def `$src` specifier to its served URL (bundleable specifiers → their
|
|
46
|
+
* `/assets/` bundle path). Receives the directory of the declaring document for relative
|
|
47
|
+
* specifiers. Identity when omitted.
|
|
48
|
+
*/
|
|
49
|
+
rewriteSrc?: (specifier: string, docDir: string | null) => string;
|
|
44
50
|
$media?: Record<string, string>;
|
|
45
51
|
formats?: FormatRegistry;
|
|
46
52
|
basePath?: string | null;
|
|
@@ -135,7 +141,13 @@ export async function compileElement(
|
|
|
135
141
|
if (optsMedia) {
|
|
136
142
|
doc.$media = { ...optsMedia, ...doc.$media };
|
|
137
143
|
}
|
|
138
|
-
const
|
|
144
|
+
const { rewriteSrc } = opts;
|
|
145
|
+
const jsContent = emitElementModule(
|
|
146
|
+
doc,
|
|
147
|
+
className,
|
|
148
|
+
elementImports,
|
|
149
|
+
rewriteSrc ? (spec: string) => rewriteSrc(spec, currentDir) : undefined,
|
|
150
|
+
);
|
|
139
151
|
const outputPath = filePath ? filePath.replace(/\.json$/, ".js") : `${tagName}.js`;
|
|
140
152
|
files.push({ content: jsContent, path: outputPath, tagName });
|
|
141
153
|
}
|
|
@@ -244,7 +256,12 @@ function extractInitialValue(def: JxMutableNode) {
|
|
|
244
256
|
* @param {string[]} elementImports
|
|
245
257
|
* @returns {string}
|
|
246
258
|
*/
|
|
247
|
-
export function emitElementModule(
|
|
259
|
+
export function emitElementModule(
|
|
260
|
+
doc: JxDocument,
|
|
261
|
+
className: string,
|
|
262
|
+
elementImports: string[],
|
|
263
|
+
rewriteSrc?: (specifier: string) => string,
|
|
264
|
+
) {
|
|
248
265
|
const lines: string[] = ["// Generated by @jxsuite/compiler — do not edit manually"];
|
|
249
266
|
|
|
250
267
|
if (doc.$id) {
|
|
@@ -269,7 +286,10 @@ export function emitElementModule(doc: JxDocument, className: string, elementImp
|
|
|
269
286
|
}
|
|
270
287
|
}
|
|
271
288
|
for (const [srcPath, names] of srcImportMap) {
|
|
272
|
-
|
|
289
|
+
// Bundleable specifiers (npm:…, ./relative) are rewritten to their /assets/ bundle URL;
|
|
290
|
+
// The site build bundles them after all documents compile (spec.md §12).
|
|
291
|
+
const importPath = rewriteSrc ? rewriteSrc(srcPath) : srcPath;
|
|
292
|
+
lines.push(`import { ${names.join(", ")} } from '${importPath}';`);
|
|
273
293
|
}
|
|
274
294
|
|
|
275
295
|
lines.push(
|
|
@@ -486,11 +506,16 @@ export function emitElementModule(doc: JxDocument, className: string, elementImp
|
|
|
486
506
|
);
|
|
487
507
|
}
|
|
488
508
|
lines.push(
|
|
509
|
+
// Lifecycle: onMount (spec §16.4) — same microtask contract as the runtime
|
|
510
|
+
" if (typeof this.state.onMount === 'function') {",
|
|
511
|
+
" queueMicrotask(() => this.state.onMount(this.state));",
|
|
512
|
+
" }",
|
|
489
513
|
" }",
|
|
490
514
|
"",
|
|
491
515
|
// DisconnectedCallback
|
|
492
516
|
" disconnectedCallback() {",
|
|
493
517
|
" if (this.#dispose) { this.#dispose(); this.#dispose = null; }",
|
|
518
|
+
" if (typeof this.state.onUnmount === 'function') { this.state.onUnmount(this.state); }",
|
|
494
519
|
" }",
|
|
495
520
|
"}",
|
|
496
521
|
"",
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
buildAttrs,
|
|
10
|
+
colorSchemePrePaintScript,
|
|
10
11
|
compileStyles,
|
|
11
12
|
createCompileContext,
|
|
12
13
|
escapeHtml,
|
|
13
14
|
isNodeDynamic,
|
|
15
|
+
pureSchemeOf,
|
|
14
16
|
resolveStaticValue,
|
|
15
17
|
SELF_CLOSING,
|
|
16
18
|
} from "../shared.ts";
|
|
@@ -31,6 +33,7 @@ export function compileStaticPage(
|
|
|
31
33
|
reactivitySrc?: string;
|
|
32
34
|
litHtmlSrc?: string;
|
|
33
35
|
projectStyle?: JxStyle | null;
|
|
36
|
+
prePaintScheme?: boolean;
|
|
34
37
|
[key: string]: unknown;
|
|
35
38
|
},
|
|
36
39
|
) {
|
|
@@ -51,7 +54,12 @@ export function compileStaticPage(
|
|
|
51
54
|
let moduleScripts = "";
|
|
52
55
|
if (islands.length > 0) {
|
|
53
56
|
for (const island of islands) {
|
|
54
|
-
const moduleContent = emitElementModule(
|
|
57
|
+
const moduleContent = emitElementModule(
|
|
58
|
+
island.def as JxDocument,
|
|
59
|
+
island.className,
|
|
60
|
+
[],
|
|
61
|
+
opts.rewriteSrc as ((specifier: string) => string) | undefined,
|
|
62
|
+
);
|
|
55
63
|
files.push({
|
|
56
64
|
content: moduleContent,
|
|
57
65
|
path: `_islands/${island.tagName}.js`,
|
|
@@ -73,13 +81,20 @@ export function compileStaticPage(
|
|
|
73
81
|
.join("\n ");
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
// Forced-scheme contract (spec §9.5): restore the persisted scheme before any styles apply
|
|
85
|
+
const prePaint =
|
|
86
|
+
opts.prePaintScheme !== false &&
|
|
87
|
+
Object.values(raw.$media ?? {}).some((q) => pureSchemeOf(String(q)) !== null)
|
|
88
|
+
? `<script>${colorSchemePrePaintScript()}</script>\n `
|
|
89
|
+
: "";
|
|
90
|
+
|
|
76
91
|
const html = `<!DOCTYPE html>
|
|
77
92
|
<html lang="en">
|
|
78
93
|
<head>
|
|
79
94
|
<meta charset="utf-8">
|
|
80
95
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
81
96
|
<title>${escapeHtml(title)}</title>
|
|
82
|
-
${importMap}
|
|
97
|
+
${prePaint}${importMap}
|
|
83
98
|
${styleBlock}
|
|
84
99
|
</head>
|
|
85
100
|
<body>
|