bosia 1.1.6 → 1.2.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/LICENSE +21 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/ambient.d.ts +3 -0
- package/src/cli/sync.ts +14 -1
- package/src/core/appHtml.ts +52 -0
- package/src/core/build.ts +4 -3
- package/src/core/client/App.svelte +82 -12
- package/src/core/client/TitleProbe.svelte +15 -0
- package/src/core/client/router.svelte.ts +1 -2
- package/src/core/client/titleMemory.svelte.ts +64 -0
- package/src/core/headScan.ts +132 -0
- package/src/core/html.ts +103 -45
- package/src/core/renderer.ts +190 -91
- package/src/core/routeFile.ts +15 -2
- package/src/core/scanner.ts +28 -31
- package/src/core/server.ts +95 -80
- package/src/lib/utils.ts +1 -8
- package/templates/default/package.json +2 -2
- package/templates/demo/package.json +2 -2
- package/templates/shop/package.json +2 -2
- package/templates/shop/src/routes/(private)/dashboard/files/+page.svelte +9 -0
- package/templates/shop/src/routes/(private)/dashboard/orders/+page.svelte +9 -0
- package/templates/shop/src/routes/(private)/dashboard/orders/pending/+page.svelte +9 -0
- package/templates/shop/src/routes/(private)/dashboard/orders/refunds/+page.svelte +9 -0
- package/templates/shop/src/routes/(private)/dashboard/products/+page.svelte +9 -0
- package/templates/shop/src/routes/(private)/dashboard/profile/+page.svelte +9 -0
- package/templates/shop/src/routes/(private)/dashboard/settings/+page.svelte +9 -0
- package/templates/shop/src/routes/(private)/dashboard/users/+page.svelte +9 -0
- package/templates/shop/src/routes/(public)/+page.svelte +2 -2
- package/templates/store/package.json +2 -2
- package/templates/store/src/routes/(private)/dashboard/files/+page.svelte +9 -0
- package/templates/store/src/routes/(private)/dashboard/orders/+page.svelte +9 -0
- package/templates/store/src/routes/(private)/dashboard/orders/pending/+page.svelte +9 -0
- package/templates/store/src/routes/(private)/dashboard/orders/refunds/+page.svelte +9 -0
- package/templates/store/src/routes/(private)/dashboard/products/+page.svelte +9 -0
- package/templates/store/src/routes/(private)/dashboard/profile/+page.svelte +9 -0
- package/templates/store/src/routes/(private)/dashboard/settings/+page.svelte +9 -0
- package/templates/store/src/routes/(private)/dashboard/users/+page.svelte +9 -0
- package/templates/store/src/routes/(public)/+page.svelte +2 -2
package/src/core/html.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { nonceAttr } from "./csp.ts";
|
|
|
5
5
|
import { rebaseHtmlAttrs } from "./basePath.ts";
|
|
6
6
|
import { currentBase } from "./appBase.ts";
|
|
7
7
|
import type { AppHtmlSegments } from "./appHtml.ts";
|
|
8
|
-
import { interpolateSegment } from "./appHtml.ts";
|
|
8
|
+
import { headSplit, interpolateSegment } from "./appHtml.ts";
|
|
9
9
|
import type { Metadata } from "./hooks.ts";
|
|
10
|
+
import { DEFAULT_TITLE, hasTitle } from "./headScan.ts";
|
|
10
11
|
|
|
11
12
|
// Workers compresses any body sent with a Content-Encoding header — again, if it
|
|
12
13
|
// already is — unless told the bytes are final. Bun ignores the key.
|
|
@@ -249,6 +250,8 @@ export function buildHtml(
|
|
|
249
250
|
segments?: AppHtmlSegments,
|
|
250
251
|
metadata?: Metadata | null,
|
|
251
252
|
pattern?: string,
|
|
253
|
+
headExtras?: string[],
|
|
254
|
+
fallbackTitle = DEFAULT_TITLE,
|
|
252
255
|
): string {
|
|
253
256
|
// An app writes <a href="/masuk">; under a base the browser has to be handed
|
|
254
257
|
// /sso/masuk or it walks off this app entirely. Only the rendered markup is
|
|
@@ -258,11 +261,11 @@ export function buildHtml(
|
|
|
258
261
|
head = rebaseHtmlAttrs(B, head);
|
|
259
262
|
|
|
260
263
|
// Metadata goes in before `head`: the first <title> in the document wins, and
|
|
261
|
-
// the
|
|
262
|
-
//
|
|
264
|
+
// buildMetadataChunk uses the same order on the streaming path, so both paths
|
|
265
|
+
// pick the same winner.
|
|
263
266
|
const metaTags = rebaseHtmlAttrs(B, metadataTags(metadata ?? null));
|
|
264
|
-
const
|
|
265
|
-
|
|
267
|
+
const extras = rebaseHtmlAttrs(B, headExtraTags(headExtras));
|
|
268
|
+
const fallback = titleFallback(segments, metadata, fallbackTitle, extras, head);
|
|
266
269
|
|
|
267
270
|
const n = nonceAttr(nonce);
|
|
268
271
|
const publicEnv = getPublicDynamicEnv();
|
|
@@ -271,14 +274,8 @@ export function buildHtml(
|
|
|
271
274
|
? `\n <script${n}>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`
|
|
272
275
|
: "";
|
|
273
276
|
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
const depsScript =
|
|
277
|
-
pageDeps !== null || layoutDeps !== null
|
|
278
|
-
? `window.__BOSIA_PAGE_DEPS__=${safeJsonStringify(pageDeps)};window.__BOSIA_LAYOUT_DEPS__=${safeJsonStringify(layoutDeps ?? [])};`
|
|
279
|
-
: "";
|
|
280
|
-
|
|
281
|
-
const sysScript = ssrFlag || depsScript ? `\n <script${n}>${ssrFlag}${depsScript}</script>` : "";
|
|
277
|
+
const sys = sysScriptBody(ssr, metadata?.title, pageDeps, layoutDeps);
|
|
278
|
+
const sysScript = sys ? `\n <script${n}>${sys}</script>` : "";
|
|
282
279
|
|
|
283
280
|
const dataIslands = csr
|
|
284
281
|
? `\n <script${n} type="application/json" id="__bosia-page-data__">${safeJsonForScript(pageData)}</script>` +
|
|
@@ -307,7 +304,6 @@ export function buildHtml(
|
|
|
307
304
|
lang: safeKey,
|
|
308
305
|
nonce,
|
|
309
306
|
});
|
|
310
|
-
const headCloseInterpolated = interpolateSegment(segments.headClose, { nonce });
|
|
311
307
|
const tailInterpolated = interpolateSegment(segments.tail, { nonce });
|
|
312
308
|
const faviconLine = segments.hasCustomFavicon
|
|
313
309
|
? ""
|
|
@@ -319,8 +315,8 @@ export function buildHtml(
|
|
|
319
315
|
componentCssLinks() +
|
|
320
316
|
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
321
317
|
preloads +
|
|
322
|
-
` ${
|
|
323
|
-
|
|
318
|
+
` ${metaTags}${extras}` +
|
|
319
|
+
closeHead(segments, lateHead(head, fallback), nonce) +
|
|
324
320
|
(body ? "" : `\n${SPINNER}`) +
|
|
325
321
|
`\n <div id="app">${body}</div>${scripts}${bodyEnd}` +
|
|
326
322
|
tailInterpolated
|
|
@@ -332,10 +328,8 @@ export function buildHtml(
|
|
|
332
328
|
<head>
|
|
333
329
|
<meta charset="UTF-8">
|
|
334
330
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
335
|
-
${fallbackTitle}
|
|
336
331
|
<link rel="icon" type="image/svg+xml" href="${FAVICON}">
|
|
337
|
-
${metaTags}
|
|
338
|
-
${twCssLink()}
|
|
332
|
+
${metaTags}${extras}${lateHead(head, fallback)} ${twCssLink()}
|
|
339
333
|
${componentCssLinks()} <script${n}>${THEME_INIT_JS}</script>
|
|
340
334
|
${preloads}</head>
|
|
341
335
|
<body>
|
|
@@ -424,29 +418,86 @@ export function metadataTags(metadata: Metadata | null): string {
|
|
|
424
418
|
return out;
|
|
425
419
|
}
|
|
426
420
|
|
|
421
|
+
/** app.html's headClose with `late` inserted just before its `</head>`, after any
|
|
422
|
+
* markup the app put after %bosia.head%, so <svelte:head> stays last in <head>
|
|
423
|
+
* and a page's styles and meta win over app.html's, as they did when it was
|
|
424
|
+
* injected client-side. No `</head>` in the segment → `late` goes first. */
|
|
425
|
+
function closeHead(segments: AppHtmlSegments, late: string, nonce?: string): string {
|
|
426
|
+
const { before, after } = headSplit(segments);
|
|
427
|
+
return (
|
|
428
|
+
(before ? interpolateSegment(before, { nonce }) : "") +
|
|
429
|
+
late +
|
|
430
|
+
interpolateSegment(after, { nonce })
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Plugin `head` fragments, one per line, empty ones skipped. */
|
|
435
|
+
function headExtraTags(headExtras?: string[]): string {
|
|
436
|
+
let out = "";
|
|
437
|
+
for (const fragment of headExtras ?? []) {
|
|
438
|
+
if (fragment) out += ` ${fragment}\n`;
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** `<title>{text}</title>` unless app.html or one of the head parts already
|
|
444
|
+
* has a title. Shared by buildHtml and buildMetadataChunk so both paths make
|
|
445
|
+
* the same call; each passes every part it writes into <head> (metadata,
|
|
446
|
+
* plugin extras, <svelte:head>). */
|
|
447
|
+
function titleFallback(
|
|
448
|
+
segments: AppHtmlSegments | undefined,
|
|
449
|
+
metadata: Metadata | null | undefined,
|
|
450
|
+
text: string,
|
|
451
|
+
...parts: string[]
|
|
452
|
+
): string {
|
|
453
|
+
if (metadata?.title || (segments && appHtmlHasTitle(segments))) return "";
|
|
454
|
+
return parts.some(hasTitle) ? "" : `<title>${escapeHtml(text)}</title>`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Whether app.html sets its own <title> in its head. */
|
|
458
|
+
export function appHtmlHasTitle(segments: AppHtmlSegments): boolean {
|
|
459
|
+
return headSplit(segments).hasTitle;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** <svelte:head>, then the fallback title, for the very end of <head>. The
|
|
463
|
+
* fallback goes last so that if hasTitle ever misses a real <title>, the real
|
|
464
|
+
* one still comes first and wins; the cost is a spare tag, not a wrong title. */
|
|
465
|
+
function lateHead(head: string, fallbackTitle: string): string {
|
|
466
|
+
return (head ? ` ${head}\n` : "") + (fallbackTitle ? ` ${fallbackTitle}\n` : "");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export type MetadataChunkOptions = {
|
|
470
|
+
/** Plugin `head` fragments; they go right after the metadata() tags. */
|
|
471
|
+
headExtras?: string[];
|
|
472
|
+
segments?: AppHtmlSegments;
|
|
473
|
+
/** <svelte:head> output; it goes last, just before app.html's </head>. */
|
|
474
|
+
head?: string;
|
|
475
|
+
nonce?: string;
|
|
476
|
+
};
|
|
477
|
+
|
|
427
478
|
/** Chunk 2: metadata tags + close </head> + open <body> + spinner */
|
|
428
479
|
export function buildMetadataChunk(
|
|
429
480
|
metadata: Metadata | null,
|
|
430
|
-
headExtras
|
|
431
|
-
segments?: AppHtmlSegments,
|
|
481
|
+
{ headExtras, segments, head = "", nonce }: MetadataChunkOptions = {},
|
|
432
482
|
): string {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
483
|
+
// `head` goes in the real <head>, not in a script, so crawlers that don't run
|
|
484
|
+
// JS still see JSON-LD, icons and og tags. Same order as buildHtml: metadata,
|
|
485
|
+
// plugin extras, app.html's own head markup, then <svelte:head>.
|
|
486
|
+
const metaTags = metadataTags(metadata);
|
|
487
|
+
const extras = headExtraTags(headExtras);
|
|
488
|
+
const fallbackTitle = titleFallback(segments, metadata, DEFAULT_TITLE, extras, head);
|
|
489
|
+
let out = "\n" + metaTags + extras;
|
|
490
|
+
const late = lateHead(head, fallbackTitle);
|
|
440
491
|
|
|
441
492
|
if (segments) {
|
|
442
|
-
|
|
443
|
-
out += headCloseInterpolated + `\n${SPINNER}`;
|
|
493
|
+
out += closeHead(segments, late, nonce) + `\n${SPINNER}`;
|
|
444
494
|
} else {
|
|
445
|
-
out +=
|
|
495
|
+
out += `${late}</head>\n<body>\n${SPINNER}`;
|
|
446
496
|
}
|
|
447
497
|
|
|
448
|
-
//
|
|
449
|
-
//
|
|
498
|
+
// No loader data islands here, so it's safe to rebase wholesale. That picks up
|
|
499
|
+
// an app's own headExtras (a canonical link, an og:image on a local file) and
|
|
500
|
+
// <svelte:head> output, which buildHtml rebases the same way.
|
|
450
501
|
return rebaseHtmlAttrs(B, out);
|
|
451
502
|
}
|
|
452
503
|
|
|
@@ -462,9 +513,24 @@ export function escapeAttr(s: string): string {
|
|
|
462
513
|
.replace(/>/g, ">");
|
|
463
514
|
}
|
|
464
515
|
|
|
516
|
+
/** The window flags hydration reads, shared by buildHtml and buildHtmlTail.
|
|
517
|
+
* `__BOSIA_TITLE__` is the title metadata() gave: navigation may replace it,
|
|
518
|
+
* but not a title a component set after it (client/App.svelte). */
|
|
519
|
+
function sysScriptBody(
|
|
520
|
+
ssr: boolean,
|
|
521
|
+
title: string | undefined,
|
|
522
|
+
pageDeps: any,
|
|
523
|
+
layoutDeps: any[] | null,
|
|
524
|
+
): string {
|
|
525
|
+
let js = ssr ? "" : "window.__BOSIA_SSR__=false;";
|
|
526
|
+
if (title) js += `window.__BOSIA_TITLE__=${safeJsonStringify(title)};`;
|
|
527
|
+
if (pageDeps !== null || layoutDeps !== null)
|
|
528
|
+
js += `window.__BOSIA_PAGE_DEPS__=${safeJsonStringify(pageDeps)};window.__BOSIA_LAYOUT_DEPS__=${safeJsonStringify(layoutDeps ?? [])};`;
|
|
529
|
+
return js;
|
|
530
|
+
}
|
|
531
|
+
|
|
465
532
|
export function buildHtmlTail(
|
|
466
533
|
body: string,
|
|
467
|
-
head: string,
|
|
468
534
|
pageData: any,
|
|
469
535
|
layoutData: any[],
|
|
470
536
|
csr: boolean,
|
|
@@ -475,16 +541,14 @@ export function buildHtmlTail(
|
|
|
475
541
|
pageDeps: any = null,
|
|
476
542
|
layoutDeps: any[] | null = null,
|
|
477
543
|
segments?: AppHtmlSegments,
|
|
544
|
+
ssrTitle?: string,
|
|
478
545
|
): string {
|
|
479
546
|
// Same rebase as buildHtml — the streamed tail carries the identical markup.
|
|
480
547
|
body = rebaseHtmlAttrs(B, body);
|
|
481
|
-
head = rebaseHtmlAttrs(B, head);
|
|
482
548
|
|
|
483
549
|
const n = nonceAttr(nonce);
|
|
484
550
|
let out = `<script${n}>document.getElementById('__bs__').remove()</script>`;
|
|
485
551
|
out += `\n<div id="app">${body}</div>`;
|
|
486
|
-
if (head)
|
|
487
|
-
out += `\n<script${n}>document.head.insertAdjacentHTML('beforeend',${safeJsonStringify(head)})</script>`;
|
|
488
552
|
if (csr) {
|
|
489
553
|
out += baseScript(nonce);
|
|
490
554
|
const publicEnv = getPublicDynamicEnv();
|
|
@@ -496,14 +560,8 @@ export function buildHtmlTail(
|
|
|
496
560
|
if (formData != null) {
|
|
497
561
|
out += `\n<script${n} type="application/json" id="__bosia-form-data__">${safeJsonForScript(formData)}</script>`;
|
|
498
562
|
}
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
pageDeps !== null || layoutDeps !== null
|
|
502
|
-
? `window.__BOSIA_PAGE_DEPS__=${safeJsonStringify(pageDeps)};window.__BOSIA_LAYOUT_DEPS__=${safeJsonStringify(layoutDeps ?? [])};`
|
|
503
|
-
: "";
|
|
504
|
-
if (ssrFlag || depsInject) {
|
|
505
|
-
out += `\n<script${n}>${ssrFlag}${depsInject}</script>`;
|
|
506
|
-
}
|
|
563
|
+
const sys = sysScriptBody(ssr, ssrTitle, pageDeps, layoutDeps);
|
|
564
|
+
if (sys) out += `\n<script${n}>${sys}</script>`;
|
|
507
565
|
out += `\n<script${n} type="module" src="${ENTRY}"></script>`;
|
|
508
566
|
} else if (isDev) {
|
|
509
567
|
out += `\n<script${n}>!function r(){var e=new EventSource("${SSE}");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`;
|