@lovrozagar/flare 0.1.2 → 0.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/README.md +42 -19
- package/package.json +8 -6
- package/src/broadcast/index.ts +1 -1
- package/src/broadcast/provider.tsx +2 -2
- package/src/caches/index.ts +26 -0
- package/src/components/await.tsx +36 -39
- package/src/components/dev-error-overlay.tsx +8 -9
- package/src/components/direction-script.tsx +2 -2
- package/src/components/locale-script.tsx +2 -2
- package/src/components/reset-css.tsx +1 -1
- package/src/components/ssr-context.tsx +7 -14
- package/src/components/theme-script.tsx +2 -2
- package/src/components/view-transition-css.tsx +1 -1
- package/src/defer/index.ts +22 -0
- package/src/direction/index.tsx +61 -36
- package/src/errors/index.ts +45 -11
- package/src/fonts/component.tsx +2 -1
- package/src/form/index.tsx +18 -12
- package/src/generators/index.ts +73 -30
- package/src/head-client/index.ts +7 -0
- package/src/history/index.ts +6 -0
- package/src/hydrate/index.tsx +90 -60
- package/src/hydration/index.ts +26 -6
- package/src/i18n/index.ts +11 -2
- package/src/image/index.tsx +7 -4
- package/src/intercept-outlet/index.tsx +6 -2
- package/src/lazy/index.tsx +50 -50
- package/src/link/index.tsx +97 -58
- package/src/loader-pipeline/index.ts +93 -35
- package/src/locale/cookie.ts +15 -0
- package/src/locale/index.tsx +24 -35
- package/src/locale.ts +1 -0
- package/src/middleware/builtins/api-proxy.ts +58 -4
- package/src/middleware/builtins/cdn-proxy.ts +24 -3
- package/src/middleware/builtins/i18n.ts +23 -25
- package/src/middleware/builtins/keepalive.ts +2 -3
- package/src/mount/index.ts +8 -1
- package/src/navigation/index.ts +575 -129
- package/src/navigation/types.ts +11 -1
- package/src/navigation-progress/index.tsx +17 -16
- package/src/ndjson-client/index.ts +42 -17
- package/src/ndjson-server/index.ts +78 -29
- package/src/outlet/index.tsx +192 -97
- package/src/outlet/types.ts +9 -1
- package/src/plugins/dev-cdn-cache.ts +18 -5
- package/src/plugins/dev-dashboard/client.tsx +130 -118
- package/src/plugins/dev-prerender.ts +6 -1
- package/src/plugins/dev-server.ts +12 -3
- package/src/plugins/image-plugin.ts +18 -4
- package/src/plugins/index.ts +10 -7
- package/src/plugins/prerender-plugin.ts +3 -3
- package/src/plugins/server-fn.ts +15 -20
- package/src/plugins/solid-jsx.d.ts +3 -3
- package/src/plugins/sx-ast/atomic.ts +5 -5
- package/src/plugins/sx-ast/index.ts +9 -0
- package/src/plugins/sx.d.ts +4 -5
- package/src/plugins/virtual.ts +11 -6
- package/src/prerender/index.ts +68 -32
- package/src/protocol.ts +65 -0
- package/src/query-client/index.tsx +73 -14
- package/src/rewrite/index.ts +10 -4
- package/src/route-builder/create-layout.ts +1 -1
- package/src/route-builder/create-root-layout.ts +1 -1
- package/src/route-builder/types.ts +5 -1
- package/src/router-primitives/types.ts +2 -0
- package/src/security/index.ts +41 -5
- package/src/server/index.ts +1 -2
- package/src/server-fn/get-input.ts +90 -0
- package/src/server-fn/http-error.ts +13 -0
- package/src/server-fn/index.ts +94 -70
- package/src/server-fn-query.ts +9 -17
- package/src/server-handler/etag.ts +4 -2
- package/src/server-handler/headers.ts +1 -2
- package/src/server-handler/index.ts +164 -56
- package/src/server-handler/types.ts +2 -0
- package/src/service-worker/template.ts +10 -42
- package/src/ssr/head-prefix.ts +2 -1
- package/src/ssr/hoist-head-markers.ts +25 -0
- package/src/ssr/index.tsx +109 -92
- package/src/state-parser/index.ts +38 -26
- package/src/styles/index.ts +89 -60
- package/src/testing/index.ts +8 -3
- package/src/theme/index.tsx +35 -32
- package/src/tracing/timing.ts +6 -1
- package/src/url/index.ts +9 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createEffect, createMemo, createSignal, For, Match,
|
|
2
|
-
import { render } from "
|
|
1
|
+
import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from "solid-js";
|
|
2
|
+
import { render } from "@solidjs/web";
|
|
3
3
|
import type { LocaleMatch } from "../../router-primitives/tree.ts";
|
|
4
4
|
import { matchRouteTree } from "./match-route-tree.ts";
|
|
5
5
|
|
|
@@ -1070,12 +1070,15 @@ function VirtualList<T>(props: {
|
|
|
1070
1070
|
}
|
|
1071
1071
|
};
|
|
1072
1072
|
|
|
1073
|
-
createEffect(
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
|
|
1073
|
+
createEffect(
|
|
1074
|
+
() => scrollEl,
|
|
1075
|
+
(el) => {
|
|
1076
|
+
if (el) {
|
|
1077
|
+
setViewHeight(el.clientHeight);
|
|
1078
|
+
if (props.containerRef) props.containerRef(el);
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
);
|
|
1079
1082
|
|
|
1080
1083
|
return (
|
|
1081
1084
|
<div
|
|
@@ -1142,9 +1145,7 @@ function TreeRow(props: {
|
|
|
1142
1145
|
>
|
|
1143
1146
|
{typeBadgeLabel(props.node.type)}
|
|
1144
1147
|
</span>
|
|
1145
|
-
<span class="tree-segment"
|
|
1146
|
-
{label()}
|
|
1147
|
-
</span>
|
|
1148
|
+
<span class={["tree-segment", { "is-group": isGroup(), "is-param": isParam() }]}>{label()}</span>
|
|
1148
1149
|
<Show when={props.node.urlPath && props.node.type !== "segment"}>
|
|
1149
1150
|
<span class="tree-url">{props.node.urlPath}</span>
|
|
1150
1151
|
</Show>
|
|
@@ -1228,7 +1229,7 @@ function ListItem(props: {
|
|
|
1228
1229
|
}
|
|
1229
1230
|
}}
|
|
1230
1231
|
role="button"
|
|
1231
|
-
|
|
1232
|
+
tabindex={0}
|
|
1232
1233
|
>
|
|
1233
1234
|
<div class="list-item-header">
|
|
1234
1235
|
<span class="list-item-url">{props.urlPath}</span>
|
|
@@ -1404,16 +1405,16 @@ function ServerFnsTab(props: {
|
|
|
1404
1405
|
<table class="fn-table">
|
|
1405
1406
|
<thead>
|
|
1406
1407
|
<tr>
|
|
1407
|
-
<th
|
|
1408
|
+
<th class={{ sorted: sortCol() === "name" }} onClick={() => handleSort("name")}>
|
|
1408
1409
|
Name{sortIndicator("name")}
|
|
1409
1410
|
</th>
|
|
1410
|
-
<th
|
|
1411
|
+
<th class={{ sorted: sortCol() === "method" }} onClick={() => handleSort("method")}>
|
|
1411
1412
|
Method{sortIndicator("method")}
|
|
1412
1413
|
</th>
|
|
1413
|
-
<th
|
|
1414
|
+
<th class={{ sorted: sortCol() === "authenticate" }} onClick={() => handleSort("authenticate")}>
|
|
1414
1415
|
Auth{sortIndicator("authenticate")}
|
|
1415
1416
|
</th>
|
|
1416
|
-
<th
|
|
1417
|
+
<th class={{ sorted: sortCol() === "stream" }} onClick={() => handleSort("stream")}>
|
|
1417
1418
|
Stream{sortIndicator("stream")}
|
|
1418
1419
|
</th>
|
|
1419
1420
|
<th>File</th>
|
|
@@ -1531,7 +1532,7 @@ function ChainNode(props: {
|
|
|
1531
1532
|
}
|
|
1532
1533
|
}}
|
|
1533
1534
|
role="button"
|
|
1534
|
-
|
|
1535
|
+
tabindex={0}
|
|
1535
1536
|
>
|
|
1536
1537
|
<div class="cur-chain-header">
|
|
1537
1538
|
<span
|
|
@@ -1543,7 +1544,7 @@ function ChainNode(props: {
|
|
|
1543
1544
|
>
|
|
1544
1545
|
{typeBadgeLabel(props.node.type)}
|
|
1545
1546
|
</span>
|
|
1546
|
-
<span class="cur-chain-seg"
|
|
1547
|
+
<span class={["cur-chain-seg", { "is-group": isGroup(), "is-param": isParam() }]}>
|
|
1547
1548
|
{props.node.segment || "/"}
|
|
1548
1549
|
</span>
|
|
1549
1550
|
<Show when={props.node.filePath}>
|
|
@@ -1667,61 +1668,67 @@ function CurrentTab(props: {
|
|
|
1667
1668
|
const [docTitle, setDocTitle] = createSignal(document.title);
|
|
1668
1669
|
|
|
1669
1670
|
/* Watch URL changes */
|
|
1670
|
-
createEffect(
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1671
|
+
createEffect(
|
|
1672
|
+
() => undefined,
|
|
1673
|
+
() => {
|
|
1674
|
+
const update = () => {
|
|
1675
|
+
setUrl({
|
|
1676
|
+
hash: window.location.hash,
|
|
1677
|
+
pathname: window.location.pathname,
|
|
1678
|
+
search: window.location.search,
|
|
1679
|
+
});
|
|
1680
|
+
setDocTitle(document.title);
|
|
1681
|
+
};
|
|
1679
1682
|
|
|
1680
|
-
|
|
1683
|
+
window.addEventListener("popstate", update);
|
|
1681
1684
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1685
|
+
const origPush = history.pushState.bind(history);
|
|
1686
|
+
const origReplace = history.replaceState.bind(history);
|
|
1687
|
+
history.pushState = (...args: Parameters<typeof history.pushState>) => {
|
|
1688
|
+
origPush(...args);
|
|
1689
|
+
update();
|
|
1690
|
+
};
|
|
1691
|
+
history.replaceState = (...args: Parameters<typeof history.replaceState>) => {
|
|
1692
|
+
origReplace(...args);
|
|
1693
|
+
update();
|
|
1694
|
+
};
|
|
1692
1695
|
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1696
|
+
return () => {
|
|
1697
|
+
window.removeEventListener("popstate", update);
|
|
1698
|
+
history.pushState = origPush;
|
|
1699
|
+
history.replaceState = origReplace;
|
|
1700
|
+
};
|
|
1701
|
+
},
|
|
1702
|
+
);
|
|
1699
1703
|
|
|
1700
1704
|
/* Watch <html> attributes + title */
|
|
1701
|
-
createEffect(
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1705
|
+
createEffect(
|
|
1706
|
+
() => undefined,
|
|
1707
|
+
() => {
|
|
1708
|
+
const observer = new MutationObserver(() => {
|
|
1709
|
+
setHtmlAttrs({
|
|
1710
|
+
dir: document.documentElement.getAttribute("dir") ?? "ltr",
|
|
1711
|
+
lang: document.documentElement.getAttribute("lang") ?? "",
|
|
1712
|
+
theme: document.documentElement.getAttribute("data-theme") ?? "",
|
|
1713
|
+
});
|
|
1714
|
+
});
|
|
1715
|
+
observer.observe(document.documentElement, {
|
|
1716
|
+
attributeFilter: ["data-theme", "dir", "lang"],
|
|
1717
|
+
attributes: true,
|
|
1707
1718
|
});
|
|
1708
|
-
});
|
|
1709
|
-
observer.observe(document.documentElement, {
|
|
1710
|
-
attributeFilter: ["data-theme", "dir", "lang"],
|
|
1711
|
-
attributes: true,
|
|
1712
|
-
});
|
|
1713
1719
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1720
|
+
const titleObs = new MutationObserver(() => setDocTitle(document.title));
|
|
1721
|
+
const titleEl = document.querySelector("title");
|
|
1722
|
+
if (titleEl) {
|
|
1723
|
+
titleObs.observe(titleEl, { characterData: true, childList: true, subtree: true });
|
|
1724
|
+
}
|
|
1719
1725
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1726
|
+
return () => {
|
|
1727
|
+
observer.disconnect();
|
|
1728
|
+
titleObs.disconnect();
|
|
1729
|
+
};
|
|
1730
|
+
},
|
|
1731
|
+
);
|
|
1725
1732
|
|
|
1726
1733
|
const matched = createMemo(() => matchRouteTree(props.tree, url().pathname, props.localeMatch));
|
|
1727
1734
|
const searchParams = createMemo(() => parseSearchParams(url().search));
|
|
@@ -1730,20 +1737,20 @@ function CurrentTab(props: {
|
|
|
1730
1737
|
const [runtimeMatches, setRuntimeMatches] = createSignal<RuntimeMatch[]>([]);
|
|
1731
1738
|
const [cachedMatches, setCachedMatches] = createSignal<CachedMatchInfo[]>([]);
|
|
1732
1739
|
|
|
1733
|
-
createEffect(
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1740
|
+
createEffect(
|
|
1741
|
+
() => url(),
|
|
1742
|
+
() => {
|
|
1743
|
+
const read = () => {
|
|
1744
|
+
const matches = window.__flare_devtools_matches__;
|
|
1745
|
+
if (matches) setRuntimeMatches(matches);
|
|
1746
|
+
const cache = window.__flare_devtools_cache__;
|
|
1747
|
+
if (cache) setCachedMatches(cache);
|
|
1748
|
+
};
|
|
1749
|
+
read();
|
|
1750
|
+
const timer = setTimeout(read, 100);
|
|
1751
|
+
return () => clearTimeout(timer);
|
|
1752
|
+
},
|
|
1753
|
+
);
|
|
1747
1754
|
|
|
1748
1755
|
const runtimeMatchFor = (node: RouteTreeNode): RuntimeMatch | undefined => {
|
|
1749
1756
|
if (!node.virtualPath) return undefined;
|
|
@@ -1785,12 +1792,14 @@ function CurrentTab(props: {
|
|
|
1785
1792
|
setHeadTags(tags);
|
|
1786
1793
|
};
|
|
1787
1794
|
|
|
1788
|
-
createEffect(
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1795
|
+
createEffect(
|
|
1796
|
+
() => url(),
|
|
1797
|
+
() => {
|
|
1798
|
+
readHeadTags();
|
|
1799
|
+
const timer = setTimeout(readHeadTags, 200);
|
|
1800
|
+
return () => clearTimeout(timer);
|
|
1801
|
+
},
|
|
1802
|
+
);
|
|
1794
1803
|
|
|
1795
1804
|
const leafMatch = createMemo(() => {
|
|
1796
1805
|
const chain = matched().chain;
|
|
@@ -2068,7 +2077,7 @@ function CopyButton(props: {
|
|
|
2068
2077
|
};
|
|
2069
2078
|
|
|
2070
2079
|
return (
|
|
2071
|
-
<button class="act-btn"
|
|
2080
|
+
<button class={["act-btn", { "is-copied": copied() }]} onClick={handleCopy} type="button">
|
|
2072
2081
|
{copied() ? "Copied" : props.label}
|
|
2073
2082
|
</button>
|
|
2074
2083
|
);
|
|
@@ -2108,11 +2117,14 @@ function ActionsTab(props: {
|
|
|
2108
2117
|
if (a) setCacheStats(a.getCacheStats());
|
|
2109
2118
|
};
|
|
2110
2119
|
|
|
2111
|
-
createEffect(
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2120
|
+
createEffect(
|
|
2121
|
+
() => undefined,
|
|
2122
|
+
() => {
|
|
2123
|
+
refreshStats();
|
|
2124
|
+
const timer = setInterval(refreshStats, 2000);
|
|
2125
|
+
return () => clearInterval(timer);
|
|
2126
|
+
},
|
|
2127
|
+
);
|
|
2116
2128
|
|
|
2117
2129
|
/* Navigate section */
|
|
2118
2130
|
const pageRoutes = createMemo(() =>
|
|
@@ -2306,8 +2318,7 @@ function ActionsTab(props: {
|
|
|
2306
2318
|
Forward
|
|
2307
2319
|
</button>
|
|
2308
2320
|
<button
|
|
2309
|
-
class="act-btn"
|
|
2310
|
-
classList={{ "is-copied": useReplace() }}
|
|
2321
|
+
class={["act-btn", { "is-copied": useReplace() }]}
|
|
2311
2322
|
onClick={() => setUseReplace((r) => !r)}
|
|
2312
2323
|
style={useReplace() ? { "border-color": "#3b82f6", color: "#3b82f6" } : {}}
|
|
2313
2324
|
type="button"
|
|
@@ -2409,8 +2420,8 @@ function DevTools(): ReturnType<(typeof import("solid-js"))["createComponent"]>
|
|
|
2409
2420
|
let searchRef: HTMLInputElement | null = null;
|
|
2410
2421
|
|
|
2411
2422
|
/* Fetch data on open */
|
|
2412
|
-
createEffect(() => {
|
|
2413
|
-
if (
|
|
2423
|
+
createEffect(isOpen, (open) => {
|
|
2424
|
+
if (open) {
|
|
2414
2425
|
fetch("/__flare/api")
|
|
2415
2426
|
.then((r) => r.json())
|
|
2416
2427
|
.then((d) => setData(d as ApiData));
|
|
@@ -2418,9 +2429,9 @@ function DevTools(): ReturnType<(typeof import("solid-js"))["createComponent"]>
|
|
|
2418
2429
|
});
|
|
2419
2430
|
|
|
2420
2431
|
/* Persist state */
|
|
2421
|
-
createEffect(() => {
|
|
2432
|
+
createEffect(activeTab, (tab) => {
|
|
2422
2433
|
saveState({
|
|
2423
|
-
activeTab:
|
|
2434
|
+
activeTab: tab,
|
|
2424
2435
|
expandedNodes: [],
|
|
2425
2436
|
sortCol: "name",
|
|
2426
2437
|
sortDir: "asc",
|
|
@@ -2428,23 +2439,26 @@ function DevTools(): ReturnType<(typeof import("solid-js"))["createComponent"]>
|
|
|
2428
2439
|
});
|
|
2429
2440
|
|
|
2430
2441
|
/* Keyboard shortcuts */
|
|
2431
|
-
createEffect(
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2442
|
+
createEffect(
|
|
2443
|
+
() => undefined,
|
|
2444
|
+
() => {
|
|
2445
|
+
const handler = (e: KeyboardEvent) => {
|
|
2446
|
+
if (e.ctrlKey && e.shiftKey && e.key === "D") {
|
|
2447
|
+
e.preventDefault();
|
|
2448
|
+
setIsOpen((o) => !o);
|
|
2449
|
+
}
|
|
2450
|
+
if (e.key === "Escape" && isOpen()) {
|
|
2451
|
+
setIsOpen(false);
|
|
2452
|
+
}
|
|
2453
|
+
if (e.key === "/" && isOpen() && document.activeElement !== searchRef) {
|
|
2454
|
+
e.preventDefault();
|
|
2455
|
+
searchRef?.focus();
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
window.addEventListener("keydown", handler);
|
|
2459
|
+
return () => window.removeEventListener("keydown", handler);
|
|
2460
|
+
},
|
|
2461
|
+
);
|
|
2448
2462
|
|
|
2449
2463
|
const tabCount = (tabId: TabId): number | null => {
|
|
2450
2464
|
const d = data();
|
|
@@ -2460,8 +2474,7 @@ function DevTools(): ReturnType<(typeof import("solid-js"))["createComponent"]>
|
|
|
2460
2474
|
return (
|
|
2461
2475
|
<>
|
|
2462
2476
|
<button
|
|
2463
|
-
class="toggle-btn"
|
|
2464
|
-
classList={{ "is-open": isOpen() }}
|
|
2477
|
+
class={["toggle-btn", { "is-open": isOpen() }]}
|
|
2465
2478
|
onClick={() => setIsOpen((o) => !o)}
|
|
2466
2479
|
title="Flare DevTools (Ctrl+Shift+D)"
|
|
2467
2480
|
type="button"
|
|
@@ -2483,8 +2496,7 @@ function DevTools(): ReturnType<(typeof import("solid-js"))["createComponent"]>
|
|
|
2483
2496
|
<For each={TABS}>
|
|
2484
2497
|
{(tab) => (
|
|
2485
2498
|
<button
|
|
2486
|
-
class="tab-btn"
|
|
2487
|
-
classList={{ active: activeTab() === tab.id }}
|
|
2499
|
+
class={["tab-btn", { active: activeTab() === tab.id }]}
|
|
2488
2500
|
onClick={() => {
|
|
2489
2501
|
setActiveTab(tab.id);
|
|
2490
2502
|
setSearch("");
|
|
@@ -48,7 +48,11 @@ export function filterDevPrerenderRoutes(defs: RouteDefinition[]): PrerenderRout
|
|
|
48
48
|
/* skip dynamic routes — they need params we don't have at dev time */
|
|
49
49
|
if (pathname.includes("[")) continue;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
const route: PrerenderRoute = { mode: "static", pathname };
|
|
52
|
+
if (def.cache.cdnTags && def.cache.cdnTags.length > 0) {
|
|
53
|
+
route.tags = def.cache.cdnTags;
|
|
54
|
+
}
|
|
55
|
+
routes.push(route);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
return routes;
|
|
@@ -103,6 +107,7 @@ export function createDevPrerenderPlugin(config: FlarePluginConfig, entries: Res
|
|
|
103
107
|
ndjson: entry.ndjson,
|
|
104
108
|
},
|
|
105
109
|
storedAt: Date.now(),
|
|
110
|
+
tags: entry.tags,
|
|
106
111
|
});
|
|
107
112
|
}
|
|
108
113
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { hoistHydrationHeadMarkers } from "../ssr/hoist-head-markers.ts";
|
|
3
4
|
import type { ResolvedEntries, VitePlugin } from "./types.ts";
|
|
4
5
|
|
|
5
6
|
interface NodeReq {
|
|
@@ -200,10 +201,18 @@ export function createDevServerPlugin(entries: ResolvedEntries, _assetsBase: str
|
|
|
200
201
|
*/
|
|
201
202
|
const lastSlash = url.pathname.lastIndexOf("/");
|
|
202
203
|
const hasFileExt = url.pathname.indexOf(".", lastSlash) !== -1;
|
|
204
|
+
let htmlPath = "/";
|
|
205
|
+
if (!hasFileExt) {
|
|
206
|
+
try {
|
|
207
|
+
decodeURIComponent(url.pathname);
|
|
208
|
+
htmlPath = url.pathname;
|
|
209
|
+
} catch {
|
|
210
|
+
htmlPath = "/";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
203
213
|
const { html: peeledHtml, tags } = peelInlineTags(html);
|
|
204
|
-
html =
|
|
205
|
-
await vite.transformIndexHtml(
|
|
206
|
-
tags,
|
|
214
|
+
html = hoistHydrationHeadMarkers(
|
|
215
|
+
restoreInlineTags(await vite.transformIndexHtml(htmlPath, peeledHtml), tags),
|
|
207
216
|
);
|
|
208
217
|
|
|
209
218
|
const htmlHeaders: Record<string, string | string[]> = {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, extname, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import type { VitePlugin } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
const IMAGE_EXTS = new Set([".avif", ".gif", ".jpeg", ".jpg", ".png", ".tiff", ".webp"]);
|
|
7
7
|
const ANIMATED_EXTS = new Set([".gif"]);
|
|
8
8
|
const IMAGE_DEFAULT_WIDTHS = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
9
|
+
export const IMAGE_MAX_WIDTH = 3840;
|
|
9
10
|
const IMAGE_DEFAULT_QUALITY = 75;
|
|
10
11
|
|
|
12
|
+
export function isAllowedImageWidth(w: number): boolean {
|
|
13
|
+
return Number.isInteger(w) && w >= 1 && w <= IMAGE_MAX_WIDTH;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
interface SharpInstance {
|
|
12
17
|
metadata: () => Promise<{ height?: number; width?: number }>;
|
|
13
18
|
resize: (width: number) => SharpInstance;
|
|
@@ -17,6 +22,14 @@ interface SharpInstance {
|
|
|
17
22
|
|
|
18
23
|
type SharpFactory = (input: string) => SharpInstance;
|
|
19
24
|
|
|
25
|
+
/** True when `target` is `root` or a descendant. Prefix match is not enough (`/proj/flare-evil` vs `/proj/flare`). */
|
|
26
|
+
export function isPathWithinRoot(target: string, root: string): boolean {
|
|
27
|
+
const rel = relative(root, target);
|
|
28
|
+
if (rel === "") return true;
|
|
29
|
+
if (isAbsolute(rel)) return false;
|
|
30
|
+
return rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
async function importSharp(): Promise<SharpFactory> {
|
|
21
34
|
try {
|
|
22
35
|
const mod = await import("sharp");
|
|
@@ -64,18 +77,19 @@ export function createImagePlugin(
|
|
|
64
77
|
const url = new URL(req.url, "http://localhost");
|
|
65
78
|
const src = url.searchParams.get("src");
|
|
66
79
|
const w = Number(url.searchParams.get("w"));
|
|
67
|
-
if (!src || !w) return next();
|
|
80
|
+
if (!src || !isAllowedImageWidth(w)) return next();
|
|
68
81
|
|
|
69
82
|
/* Guard against path traversal — resolve symlinks then check within cwd */
|
|
83
|
+
const cwd = process.cwd();
|
|
70
84
|
const resolvedSrc = resolve(src);
|
|
71
|
-
if (!resolvedSrc
|
|
85
|
+
if (!isPathWithinRoot(resolvedSrc, cwd)) return next();
|
|
72
86
|
let realSrc: string;
|
|
73
87
|
try {
|
|
74
88
|
realSrc = realpathSync(resolvedSrc);
|
|
75
89
|
} catch {
|
|
76
90
|
return next();
|
|
77
91
|
}
|
|
78
|
-
if (!realSrc
|
|
92
|
+
if (!isPathWithinRoot(realSrc, cwd)) return next();
|
|
79
93
|
|
|
80
94
|
const sharp = await importSharp();
|
|
81
95
|
const sourceExt = extname(realSrc).toLowerCase();
|
package/src/plugins/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, watch } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import type { Options as SolidPluginOptions } from "vite-plugin
|
|
5
|
-
import solid from "vite-plugin
|
|
4
|
+
import type { Options as SolidPluginOptions } from "@solidjs/vite-plugin";
|
|
5
|
+
import solid from "@solidjs/vite-plugin";
|
|
6
6
|
import { runGenerate } from "../generators/index.ts";
|
|
7
7
|
import { EMPTY_OBJ } from "../internal/index.ts";
|
|
8
8
|
import type { LogLevel } from "../logger.ts";
|
|
@@ -285,11 +285,11 @@ function createSsrBuildPlugin(
|
|
|
285
285
|
},
|
|
286
286
|
},
|
|
287
287
|
optimizeDeps: {
|
|
288
|
-
include: ["solid-js", "
|
|
288
|
+
include: ["solid-js", "@solidjs/web"],
|
|
289
289
|
},
|
|
290
290
|
resolve: {
|
|
291
291
|
...(config.alias ? { alias: config.alias } : {}),
|
|
292
|
-
dedupe: ["solid-js", "
|
|
292
|
+
dedupe: ["solid-js", "@solidjs/web"],
|
|
293
293
|
},
|
|
294
294
|
server: {
|
|
295
295
|
...(config.port ? { port: config.port } : {}),
|
|
@@ -298,7 +298,7 @@ function createSsrBuildPlugin(
|
|
|
298
298
|
},
|
|
299
299
|
},
|
|
300
300
|
ssr: {
|
|
301
|
-
noExternal: ["solid-js", "@lovrozagar/flare", "@tanstack/solid-query"],
|
|
301
|
+
noExternal: ["solid-js", "@solidjs/web", "@lovrozagar/flare", "@tanstack/solid-query"],
|
|
302
302
|
},
|
|
303
303
|
};
|
|
304
304
|
},
|
|
@@ -386,16 +386,19 @@ export function flare(config: FlarePluginConfig = EMPTY_OBJ): VitePlugin[] {
|
|
|
386
386
|
server: resolveEntry(root, "server", config.entry?.server),
|
|
387
387
|
};
|
|
388
388
|
|
|
389
|
+
if (config.solid && "start" in config.solid && (config.solid as { start?: unknown }).start) {
|
|
390
|
+
throw new Error("Flare: solid.start is not supported. Flare owns entries, SSR, and server functions.");
|
|
391
|
+
}
|
|
392
|
+
|
|
389
393
|
const solidConfig = {
|
|
390
394
|
extensions: [".tsx", ".jsx"],
|
|
391
|
-
hydratable: true,
|
|
392
395
|
ssr: true,
|
|
393
396
|
...config.solid,
|
|
394
397
|
};
|
|
395
398
|
|
|
396
399
|
const resolvedSw = normalizeSwConfig(config.serviceWorker);
|
|
397
400
|
|
|
398
|
-
/* TS 7: vite + vite-plugin
|
|
401
|
+
/* TS 7: vite + @solidjs/vite-plugin Plugin identities overflow. Keep this list on VitePlugin. */
|
|
399
402
|
const plugins: VitePlugin[] = [
|
|
400
403
|
createTwDeprecatedPlugin(),
|
|
401
404
|
/* manifest: true always enabled so virtual:flare-sx-manifest is populated at SSR build time */
|
|
@@ -92,13 +92,13 @@ export function createPrerenderPlugin(config: {
|
|
|
92
92
|
const mod = (await import(`${serverPath}?t=${Date.now()}`)) as ServerModule;
|
|
93
93
|
|
|
94
94
|
/* 4. Resolve static params for dynamic routes */
|
|
95
|
-
const
|
|
96
|
-
(d) => d.type === "page" && (d.cache.ssg
|
|
95
|
+
const needsStaticParams = defs.some(
|
|
96
|
+
(d) => d.type === "page" && Boolean(d.cache.ssg || d.cache.isr) && d.virtualPath.includes("["),
|
|
97
97
|
);
|
|
98
98
|
const resolvedHandler = mod.server ?? mod.handler;
|
|
99
99
|
if (!resolvedHandler) return;
|
|
100
100
|
const staticParams =
|
|
101
|
-
|
|
101
|
+
needsStaticParams && resolvedHandler.getStaticParams ? await resolvedHandler.getStaticParams() : undefined;
|
|
102
102
|
|
|
103
103
|
/* 5. Convert to prerender routes */
|
|
104
104
|
const routes = buildPrerenderRoutes(defs, staticParams);
|
package/src/plugins/server-fn.ts
CHANGED
|
@@ -412,34 +412,29 @@ function stripCommentsAndStrings(code: string): string {
|
|
|
412
412
|
* Preserves _registration (client needs id, name, method for RPC).
|
|
413
413
|
* Uses paren-depth tracking to find matching closing paren.
|
|
414
414
|
*/
|
|
415
|
+
const CLIENT_STUB_TOKENS = [".handler(", ".stream(", ".authorize(", ".input("] as const;
|
|
416
|
+
|
|
417
|
+
function nextClientStub(code: string, from: number): { idx: number; token: string } | null {
|
|
418
|
+
let best: { idx: number; token: string } | null = null;
|
|
419
|
+
for (const token of CLIENT_STUB_TOKENS) {
|
|
420
|
+
const idx = code.indexOf(token, from);
|
|
421
|
+
if (idx === -1) continue;
|
|
422
|
+
if (!best || idx < best.idx) best = { idx, token };
|
|
423
|
+
}
|
|
424
|
+
return best;
|
|
425
|
+
}
|
|
426
|
+
|
|
415
427
|
export function stripHandlerBodies(code: string): string {
|
|
416
428
|
let result = "";
|
|
417
429
|
let cursor = 0;
|
|
418
430
|
|
|
419
431
|
while (cursor < code.length) {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
/* pick whichever comes first, skip -1 (not found) */
|
|
424
|
-
let idx: number;
|
|
425
|
-
let token: string;
|
|
426
|
-
if (handlerIdx === -1 && streamIdx === -1) {
|
|
432
|
+
const found = nextClientStub(code, cursor);
|
|
433
|
+
if (!found) {
|
|
427
434
|
result += code.slice(cursor);
|
|
428
435
|
break;
|
|
429
436
|
}
|
|
430
|
-
|
|
431
|
-
idx = streamIdx;
|
|
432
|
-
token = ".stream(";
|
|
433
|
-
} else if (streamIdx === -1) {
|
|
434
|
-
idx = handlerIdx;
|
|
435
|
-
token = ".handler(";
|
|
436
|
-
} else if (handlerIdx <= streamIdx) {
|
|
437
|
-
idx = handlerIdx;
|
|
438
|
-
token = ".handler(";
|
|
439
|
-
} else {
|
|
440
|
-
idx = streamIdx;
|
|
441
|
-
token = ".stream(";
|
|
442
|
-
}
|
|
437
|
+
const { idx, token } = found;
|
|
443
438
|
|
|
444
439
|
/* include everything up to and including the token */
|
|
445
440
|
const handlerStart = idx + token.length;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import "@solidjs/web";
|
|
2
2
|
|
|
3
|
-
declare module "
|
|
3
|
+
declare module "@solidjs/web" {
|
|
4
4
|
namespace JSX {
|
|
5
5
|
interface HTMLAttributes<T> {
|
|
6
6
|
css?: string;
|
|
7
7
|
}
|
|
8
|
-
interface
|
|
8
|
+
interface SVGAttributes<T> {
|
|
9
9
|
css?: string;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -28,12 +28,12 @@ function hash4(s: string): string {
|
|
|
28
28
|
* Class name triple: selector\0property\0value.
|
|
29
29
|
* Null-byte separators prevent `color:red` ↔ `col:or:red` collisions.
|
|
30
30
|
*/
|
|
31
|
-
function tripleKey(selector: string, prop: string, value: string): string {
|
|
32
|
-
return `${selector}\0${prop}\0${value}`;
|
|
31
|
+
function tripleKey(selector: string, prop: string, value: string, atWrap = ""): string {
|
|
32
|
+
return `${selector}\0${prop}\0${value}\0${atWrap}`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function className(selector: string, prop: string, value: string, mode: "dev" | "prod"): string {
|
|
36
|
-
const key = tripleKey(selector, prop, value);
|
|
35
|
+
function className(selector: string, prop: string, value: string, mode: "dev" | "prod", atWrap = ""): string {
|
|
36
|
+
const key = tripleKey(selector, prop, value, atWrap);
|
|
37
37
|
if (mode === "prod") {
|
|
38
38
|
return `a1-${hash8(key)}`;
|
|
39
39
|
}
|
|
@@ -101,7 +101,7 @@ function emitDeclarations(ctx: EmitCtx, cssSelector: string, base: SxIR["base"],
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
const cssProp = toKebab(prop);
|
|
104
|
-
const cls = className(cssSelector, cssProp, value, ctx.mode);
|
|
104
|
+
const cls = className(cssSelector, cssProp, value, ctx.mode, atWrap);
|
|
105
105
|
ctx.classes.push(cls);
|
|
106
106
|
|
|
107
107
|
if (ctx.cssRules.has(cls)) continue;
|