Skip to content

Commit 1de566d

Browse files
committed
feat: implement unified useAssets css mounting API
1 parent a6263cf commit 1de566d

8 files changed

Lines changed: 87 additions & 75 deletions

File tree

packages/start/src/server/StartServer.tsx

Lines changed: 7 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,21 @@
11
// @refresh skip
2-
import App from "solid-start:app";
3-
import type { Component, JSX } from "solid-js";
2+
import type { Component } from "solid-js";
43
import {
54
Hydration,
65
HydrationScript,
76
NoHydration,
87
getRequestEvent,
9-
ssr,
10-
useAssets
8+
ssr
119
} from "solid-js/web";
10+
import App from "solid-start:app";
1211

1312
import { ErrorBoundary, TopErrorBoundary } from "../shared/ErrorBoundary.tsx";
14-
import { renderAsset } from "./renderAsset.tsx";
15-
import type { Asset, DocumentComponentProps, PageEvent } from "./types.ts";
13+
import { useAssets } from "./assets/index.ts";
1614
import { getSsrManifest } from "./manifest/ssr-manifest.ts";
15+
import type { DocumentComponentProps, PageEvent } from "./types.ts";
1716

1817
const docType = ssr("<!DOCTYPE html>");
1918

20-
function matchRoute(matches: any[], routes: any[], matched = []): any[] | undefined {
21-
for (let i = 0; i < routes.length; i++) {
22-
const segment = routes[i];
23-
if (segment.path !== matches[0].path) continue;
24-
let next: any = [...matched, segment];
25-
if (segment.children) {
26-
const nextMatches = matches.slice(1);
27-
if (nextMatches.length === 0) continue;
28-
next = matchRoute(nextMatches, segment.children, next);
29-
if (!next) continue;
30-
}
31-
return next;
32-
}
33-
}
34-
3519
/**
3620
*
3721
* Read more: https://docs.solidjs.com/solid-start/reference/server/start-server
@@ -41,60 +25,16 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
4125

4226
// @ts-ignore
4327
const nonce = context.nonce;
44-
45-
let assets: Asset[] = [];
46-
Promise.resolve()
47-
.then(async () => {
48-
const manifest = getSsrManifest("ssr");
49-
50-
let assetPromises: Promise<Asset[]>[] = [];
51-
// @ts-ignore
52-
if (context.router && context.router.matches) {
53-
// @ts-ignore
54-
const matches = [...context.router.matches];
55-
while (matches.length && (!matches[0].info || !matches[0].info.filesystem)) matches.shift();
56-
const matched = matches.length && matchRoute(matches, context.routes);
57-
if (matched) {
58-
for (let i = 0; i < matched.length; i++) {
59-
const segment = matched[i];
60-
assetPromises.push(manifest.getAssets(segment["$component"].src));
61-
}
62-
} else if (import.meta.env.DEV)
63-
console.warn(
64-
`No route matched for preloading js assets for path ${new URL(context.request.url).pathname}`
65-
);
66-
}
67-
assets = await Promise.all(assetPromises).then(a =>
68-
// dedupe assets
69-
[...new Map(a.flat().map(item => [item.attrs.key, item])).values()].filter(asset =>
70-
import.meta.env.START_ISLANDS
71-
? false
72-
: (asset.attrs as JSX.LinkHTMLAttributes<HTMLLinkElement>).rel === "modulepreload" &&
73-
!context.assets.find((a: Asset) => a.attrs.key === asset.attrs.key)
74-
)
75-
);
76-
})
77-
.catch(console.error);
78-
79-
useAssets(() => (assets.length ? assets.map(m => renderAsset(m)) : undefined));
28+
useAssets(context.assets, nonce);
8029

8130
return (
8231
<NoHydration>
8332
{docType as unknown as any}
8433
<TopErrorBoundary>
8534
<props.document
86-
assets={
87-
<>
88-
<HydrationScript />
89-
{context.assets.map((m: any) => renderAsset(m, nonce))}
90-
</>
91-
}
35+
assets={<HydrationScript />}
9236
scripts={
9337
<>
94-
<script
95-
nonce={nonce}
96-
innerHTML={`window.manifest = ${JSON.stringify(context.manifest)}`}
97-
/>
9838
<script
9939
type="module"
10040
nonce={nonce}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { onCleanup, sharedConfig } from "solid-js";
2+
import { getRequestEvent, useAssets as useAssets_ } from "solid-js/web";
3+
import { renderAsset, type Asset } from "./render.tsx";
4+
5+
const REGISTRY = Symbol("assetRegistry");
6+
const NOOP = () => "";
7+
8+
type AssetEntity = {
9+
key: string;
10+
consumers: number;
11+
preloadEl?: HTMLLinkElement;
12+
ssrIdx?: number;
13+
};
14+
type Registry = Record<string, AssetEntity>;
15+
16+
const keyAttrs = ["href", "rel", "data-vite-dev-id"] as const;
17+
18+
const getEntity = (registry: Registry, asset: Asset) => {
19+
let key = asset.tag;
20+
for (const k of keyAttrs) {
21+
if (!(k in asset.attrs)) continue;
22+
key += `[${k}='${asset.attrs[k as keyof Asset["attrs"]]}']`;
23+
}
24+
25+
const entity = (registry[key] ??= {
26+
key,
27+
consumers: 0
28+
});
29+
30+
return entity;
31+
};
32+
33+
export const useAssets = (assets: Asset[], nonce?: string) => {
34+
if (!assets.length) return;
35+
36+
const registry: Registry = (getRequestEvent()!.locals[REGISTRY] ??= {});
37+
const ssrRequestAssets: Function[] = (sharedConfig.context as any)?.assets;
38+
const cssKeys: string[] = [];
39+
40+
for (const asset of assets) {
41+
const entity = getEntity(registry, asset);
42+
const isCSSLink = asset.tag === "link" && asset.attrs.rel === "stylesheet";
43+
const isCSS = isCSSLink || asset.tag === "style";
44+
if (isCSS) {
45+
cssKeys.push(entity.key);
46+
}
47+
48+
entity.consumers++;
49+
if (entity.consumers > 1) continue;
50+
51+
// Mounting logic
52+
useAssets_(() => renderAsset(asset, nonce));
53+
entity.ssrIdx = ssrRequestAssets.length - 1;
54+
}
55+
56+
onCleanup(() => {
57+
for (const key of cssKeys) {
58+
const entity = registry[key]!;
59+
entity.consumers--;
60+
if (entity.consumers != 0) {
61+
continue;
62+
}
63+
64+
// Ideally this logic should be implemented directly in dom-expressions
65+
ssrRequestAssets.splice(entity.ssrIdx!, 1, NOOP);
66+
67+
delete registry[key];
68+
}
69+
});
70+
};
File renamed without changes.

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 { useAssets } from "../shared/assets.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+
useAssets(styles);
63+
6264
return [
63-
...styles.map((asset: Asset) => renderAsset(asset)),
6465
createComponent(Component, props)
6566
];
6667
};

packages/start/src/server/manifest/prod-ssr-manifest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { clientViteManifest } from "solid-start:client-vite-manifest";
22
import { join } from "pathe";
33
import { Manifest } from "vite";
4-
import type { Asset } from "../renderAsset.tsx";
4+
import type { Asset } from "../assets/render.tsx";
55

66
// Only reads from client manifest atm, might need server support for islands
77
export function getSsrProdManifest() {
@@ -61,7 +61,7 @@ function createHtmlTagsForAssets(assets: string[]) {
6161
href: '/' + asset,
6262
key: asset,
6363
...(asset.endsWith(".css")
64-
? { rel: "stylesheet", fetchPriority: "high" }
64+
? { rel: "stylesheet" }
6565
: { rel: "modulepreload" }),
6666
},
6767
}));

packages/start/src/server/spa/StartServer.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { NoHydration, getRequestEvent, ssr } from "solid-js/web";
55
import { getSsrManifest } from "../manifest/ssr-manifest.ts";
66

77
import { TopErrorBoundary } from "../../shared/ErrorBoundary.tsx";
8-
import { renderAsset } from "../renderAsset.tsx";
8+
import { useAssets } from "../assets/index.ts";
99
import type { DocumentComponentProps, PageEvent } from "../types.ts";
1010

1111
const docType = ssr("<!DOCTYPE html>");
@@ -18,12 +18,13 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
1818
const context = getRequestEvent() as PageEvent;
1919
// @ts-ignore
2020
const nonce = context.nonce;
21+
useAssets(context.assets, nonce);
22+
2123
return (
2224
<NoHydration>
2325
{docType as unknown as any}
2426
<TopErrorBoundary>
2527
<props.document
26-
assets={<>{context.assets.map((m: any) => renderAsset(m))}</>}
2728
scripts={
2829
<>
2930
<script

packages/start/src/server/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { RequestEvent } from "solid-js/web";
55
// export const FETCH_EVENT = "$FETCH";
66

77
export type DocumentComponentProps = {
8-
assets: JSX.Element;
8+
assets?: JSX.Element;
99
scripts: JSX.Element;
1010
children?: JSX.Element;
1111
};

packages/start/src/virtual.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ declare module "solid-start:client-vite-manifest" {
66
}
77

88
interface StartManifest {
9-
getAssets(id: string): Promise<import("./server/renderAsset").Asset[]>;
9+
getAssets(id: string): Promise<import("./server/assets/render").Asset[]>;
1010
}
1111

1212
declare module "solid-start:get-client-manifest" {

0 commit comments

Comments
 (0)