Skip to content

Commit 7945276

Browse files
committed
feat: rework css mounting (pt2)
1 parent e52ade7 commit 7945276

File tree

2 files changed

+86
-54
lines changed

2 files changed

+86
-54
lines changed

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/shared/css.ts

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,101 @@
11
import { createRenderEffect, createResource, onCleanup, sharedConfig } from "solid-js";
2-
import { isServer, useAssets } from "solid-js/web";
2+
import { getRequestEvent, isServer, useAssets } from "solid-js/web";
33
import { renderAsset, type Asset } from "../server/renderAsset.tsx";
44

5-
const instances: Record<string, { uses: number; el: HTMLElement }> = {};
6-
export const mountAssets = (assets: Asset[]) => {
7-
if (!assets.length) return;
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"];
812

9-
if (isServer) {
10-
useAssets(() => assets.map((asset: Asset) => renderAsset(asset)));
11-
const a: [] = (sharedConfig.context as any).assets;
12-
const index = a.length - 1;
13-
onCleanup(() => {
14-
// TODO: index is not properly tracked!
15-
a.splice(index, assets.length);
16-
});
17-
return;
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"]]}']`;
1820
}
1921

20-
const keys: string[] = [];
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+
2143
for (const asset of assets) {
22-
const attrs = Object.entries(asset.attrs);
23-
let key = asset.tag;
24-
for (const [k, v] of attrs) {
25-
if (k === "key") continue;
26-
key += `[${k}='${v}']`;
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);
2748
}
28-
keys.push(key);
2949

30-
console.log("mount", key);
50+
entity.consumers++;
51+
if (entity.consumers > 1 || entity.el) continue;
3152

32-
const ssrEl = document.querySelector("head " + key) as HTMLElement;
33-
const instance = (instances[key] ??= {
34-
uses: 0,
35-
el: ssrEl ?? document.createElement(asset.tag)
36-
});
37-
instance.uses++;
38-
39-
if (instance.uses > 1 || ssrEl) continue;
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));
4062

41-
const [r] = createResource(() => {
42-
return new Promise(res => {
43-
instance.el.addEventListener("load", res, { once: true });
44-
instance.el.addEventListener("error", res, { once: true });
45-
});
46-
});
47-
createRenderEffect(r);
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+
}
4873

49-
for (const [k, v] of attrs) {
50-
if (k === "key") continue;
51-
instance.el.setAttribute(k, v);
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);
5279
}
53-
document.head.append(instance.el);
5480
}
5581

82+
// Unmounting logic
83+
if (!unmount) return;
5684
onCleanup(() => {
57-
for (const key of keys) {
58-
const instance = instances[key]!;
59-
instance.uses--;
60-
if (instance.uses === 0) {
61-
console.log("unmount", key);
62-
instance.el.remove();
63-
delete instances[key];
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];
6499
}
65100
}
66101
});

0 commit comments

Comments
 (0)