Skip to content

Commit c9dd38e

Browse files
committed
feat: rework route css mounting
1 parent c98aaf6 commit c9dd38e

File tree

4 files changed

+110
-9
lines changed

4 files changed

+110
-9
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export { mountAssets } from "../shared/css.ts";
12
export { mount } from "./mount.ts";
23
export { StartClient, StartClientTanstack } from "./StartClient.tsx";

packages/start/src/server/StartServer.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import {
1010
useAssets
1111
} from "solid-js/web";
1212

13+
import { mountAssets } from "../shared/css.ts";
1314
import { ErrorBoundary, TopErrorBoundary } from "../shared/ErrorBoundary.tsx";
15+
import { getSsrManifest } from "./manifest/ssr-manifest.ts";
1416
import { renderAsset } from "./renderAsset.tsx";
1517
import type { Asset, DocumentComponentProps, PageEvent } from "./types.ts";
16-
import { getSsrManifest } from "./manifest/ssr-manifest.ts";
1718

1819
const docType = ssr("<!DOCTYPE html>");
1920

@@ -77,18 +78,14 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
7778
.catch(console.error);
7879

7980
useAssets(() => (assets.length ? assets.map(m => renderAsset(m)) : undefined));
81+
mountAssets(context.assets, { unmount: false, nonce });
8082

8183
return (
8284
<NoHydration>
8385
{docType as unknown as any}
8486
<TopErrorBoundary>
8587
<props.document
86-
assets={
87-
<>
88-
<HydrationScript />
89-
{context.assets.map((m: any) => renderAsset(m, nonce))}
90-
</>
91-
}
88+
assets={<HydrationScript />}
9289
scripts={
9390
<>
9491
<script

packages/start/src/server/lazyRoute.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type Component, createComponent, type JSX, lazy, onCleanup } from "solid-js";
2-
2+
import { mountAssets } from "../shared/css.ts";
33
import { type Asset, renderAsset } from "./renderAsset.tsx";
44

55
export default function lazyRoute<T extends Record<string, any>>(
@@ -59,8 +59,9 @@ export default function lazyRoute<T extends Record<string, any>>(
5959
preloadStyles(styles);
6060
}
6161
const Comp: Component<T> = props => {
62+
mountAssets(styles);
63+
6264
return [
63-
...styles.map((asset: Asset) => renderAsset(asset)),
6465
createComponent(Component, props)
6566
];
6667
};

packages/start/src/shared/css.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { createRenderEffect, createResource, onCleanup, sharedConfig } from "solid-js";
2+
import { getRequestEvent, isServer, useAssets } from "solid-js/web";
3+
import { renderAsset, type Asset } from "../server/renderAsset.tsx";
4+
5+
const CANCEL_EVENT = "cancel";
6+
const EVENT_REGISTRY = Symbol("assetRegistry");
7+
const NOOP = () => "";
8+
9+
type AssetEntity = { key: string; consumers: number; el?: HTMLElement; ssrIdx?: number };
10+
type Registry = Record<string, AssetEntity>;
11+
type AttrKeys = keyof Asset["attrs"];
12+
13+
const globalRegistry: Registry = {};
14+
15+
const getEntity = (registry: Registry, asset: Asset) => {
16+
let key = asset.tag;
17+
for (const k of Object.keys(asset.attrs)) {
18+
if (k === "key") continue;
19+
key += `[${k}='${asset.attrs[k as keyof Asset["attrs"]]}']`;
20+
}
21+
22+
const entity = (registry[key] ??= {
23+
key,
24+
consumers: 0,
25+
el: isServer ? undefined : (document.querySelector("head " + key) as HTMLElement)
26+
});
27+
28+
return entity;
29+
};
30+
31+
export const mountAssets = (
32+
assets: Asset[],
33+
{ unmount = true, nonce }: { unmount?: boolean; nonce?: string } = {}
34+
) => {
35+
if (!assets.length) return;
36+
37+
const registry: Registry = isServer
38+
? (getRequestEvent()!.locals[EVENT_REGISTRY] ??= {})
39+
: globalRegistry;
40+
const ssrRequestAssets: Function[] = (sharedConfig.context as any)?.assets;
41+
const cssKeys: string[] = [];
42+
43+
for (const asset of assets) {
44+
const entity = getEntity(registry, asset);
45+
const isCSS = asset.tag === "link" && asset.attrs.rel === "stylesheet";
46+
if (isCSS && entity.el?.dataset.keep != "") {
47+
cssKeys.push(entity.key);
48+
}
49+
50+
entity.consumers++;
51+
if (entity.consumers > 1 || entity.el) continue;
52+
53+
// Mounting logic
54+
if (isServer) {
55+
if (!unmount && isCSS) {
56+
asset.attrs["data-keep" as AttrKeys] = "";
57+
}
58+
useAssets(() => renderAsset(asset, nonce));
59+
entity.ssrIdx = ssrRequestAssets.length - 1;
60+
} else {
61+
const el = (entity.el = document.createElement(asset.tag));
62+
63+
if (isCSS) {
64+
const [r] = createResource(() => {
65+
return new Promise(res => {
66+
el.addEventListener("load", res, { once: true });
67+
el.addEventListener(CANCEL_EVENT, res, { once: true });
68+
el.addEventListener("error", res, { once: true });
69+
});
70+
});
71+
createRenderEffect(r);
72+
}
73+
74+
for (const k of Object.keys(asset.attrs)) {
75+
if (k === "key") continue;
76+
el.setAttribute(k, asset.attrs[k as AttrKeys]);
77+
}
78+
document.head.appendChild(el);
79+
}
80+
}
81+
82+
// Unmounting logic
83+
if (!unmount) return;
84+
onCleanup(() => {
85+
for (const key of cssKeys) {
86+
const entity = registry[key]!;
87+
entity.consumers--;
88+
if (entity.consumers != 0) {
89+
continue;
90+
}
91+
92+
if (isServer) {
93+
// Ideally this logic should be implemented directly in dom-expressions
94+
ssrRequestAssets.splice(entity.ssrIdx!, 1, NOOP);
95+
} else {
96+
entity.el!.dispatchEvent(new CustomEvent(CANCEL_EVENT));
97+
entity.el!.remove();
98+
delete registry[key];
99+
}
100+
}
101+
});
102+
};

0 commit comments

Comments
 (0)