@chr33s/solarflare 0.0.2

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.
Files changed (47) hide show
  1. package/package.json +52 -0
  2. package/readme.md +183 -0
  3. package/src/ast.ts +316 -0
  4. package/src/build.bundle-client.ts +404 -0
  5. package/src/build.bundle-server.ts +131 -0
  6. package/src/build.bundle.ts +48 -0
  7. package/src/build.emit-manifests.ts +25 -0
  8. package/src/build.hmr-entry.ts +88 -0
  9. package/src/build.scan.ts +182 -0
  10. package/src/build.ts +227 -0
  11. package/src/build.validate.ts +63 -0
  12. package/src/client.hmr.ts +78 -0
  13. package/src/client.styles.ts +68 -0
  14. package/src/client.ts +190 -0
  15. package/src/codemod.ts +688 -0
  16. package/src/console-forward.ts +254 -0
  17. package/src/critical-css.ts +103 -0
  18. package/src/devtools-json.ts +52 -0
  19. package/src/diff-dom-streaming.ts +406 -0
  20. package/src/early-flush.ts +125 -0
  21. package/src/early-hints.ts +83 -0
  22. package/src/fetch.ts +44 -0
  23. package/src/fs.ts +11 -0
  24. package/src/head.ts +876 -0
  25. package/src/hmr.ts +647 -0
  26. package/src/hydration.ts +238 -0
  27. package/src/manifest.runtime.ts +25 -0
  28. package/src/manifest.ts +23 -0
  29. package/src/paths.ts +96 -0
  30. package/src/render-priority.ts +69 -0
  31. package/src/route-cache.ts +163 -0
  32. package/src/router-deferred.ts +85 -0
  33. package/src/router-stream.ts +65 -0
  34. package/src/router.ts +535 -0
  35. package/src/runtime.ts +32 -0
  36. package/src/serialize.ts +38 -0
  37. package/src/server.hmr.ts +67 -0
  38. package/src/server.styles.ts +42 -0
  39. package/src/server.ts +480 -0
  40. package/src/solarflare.d.ts +101 -0
  41. package/src/speculation-rules.ts +171 -0
  42. package/src/store.ts +78 -0
  43. package/src/stream-assets.ts +135 -0
  44. package/src/stylesheets.ts +222 -0
  45. package/src/worker.config.ts +243 -0
  46. package/src/worker.ts +542 -0
  47. package/tsconfig.json +21 -0
@@ -0,0 +1,182 @@
1
+ import { glob, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import { exists } from "./fs.ts";
5
+
6
+ export interface BuildScanContext {
7
+ rootDir: string;
8
+ appDir: string;
9
+ }
10
+
11
+ export function createScanner(ctx: BuildScanContext) {
12
+ const { rootDir, appDir } = ctx;
13
+ let packageImportsCache: Record<string, string> | null = null;
14
+
15
+ async function scanFiles(pattern: string, cwd: string) {
16
+ const files: string[] = [];
17
+ for await (const file of glob(pattern, { cwd, withFileTypes: false })) {
18
+ files.push(file as string);
19
+ }
20
+ return files.sort();
21
+ }
22
+
23
+ async function getPackageImports() {
24
+ if (packageImportsCache) return packageImportsCache;
25
+
26
+ const pkgPath = join(rootDir, "package.json");
27
+ try {
28
+ const content = await readFile(pkgPath, "utf-8");
29
+ const pkg = JSON.parse(content) as { imports?: Record<string, string> };
30
+ const imports = pkg.imports ?? {};
31
+
32
+ packageImportsCache = {};
33
+ for (const [key, value] of Object.entries(imports)) {
34
+ const aliasKey = key.replace(/\/\*$/, "");
35
+ const aliasValue = value.replace(/\/\*$/, "");
36
+ packageImportsCache[aliasKey] = aliasValue.startsWith(".")
37
+ ? join(rootDir, aliasValue)
38
+ : aliasValue;
39
+ }
40
+ return packageImportsCache;
41
+ } catch {
42
+ packageImportsCache = { "#app": appDir };
43
+ return packageImportsCache;
44
+ }
45
+ }
46
+
47
+ async function findRouteModules() {
48
+ return scanFiles("**/*.{client,server}.{ts,tsx}", appDir);
49
+ }
50
+
51
+ async function findLayouts() {
52
+ return scanFiles("**/_layout.tsx", appDir);
53
+ }
54
+
55
+ async function findErrorFile() {
56
+ const files = await scanFiles("_error.tsx", appDir);
57
+ return files.length > 0 ? files[0] : null;
58
+ }
59
+
60
+ async function findClientComponents() {
61
+ return scanFiles("**/*.client.tsx", appDir);
62
+ }
63
+
64
+ async function extractCssImports(filePath: string) {
65
+ const content = await readFile(filePath, "utf-8");
66
+ const cssImports: string[] = [];
67
+
68
+ const importRegex = /import\s+['"](.+\.css)['"]|import\s+['"](.+\.css)['"]\s*;/g;
69
+ let match;
70
+ while ((match = importRegex.exec(content)) !== null) {
71
+ const cssPath = match[1] || match[2];
72
+ if (cssPath) {
73
+ cssImports.push(cssPath);
74
+ }
75
+ }
76
+
77
+ return cssImports;
78
+ }
79
+
80
+ async function extractComponentImports(filePath: string) {
81
+ const content = await readFile(filePath, "utf-8");
82
+ const imports: string[] = [];
83
+
84
+ const importRegex = /import\s+(?:{[^}]+}|\w+)\s+from\s+['"]([^'"]+)['"]/g;
85
+ let match;
86
+ while ((match = importRegex.exec(content)) !== null) {
87
+ const importPath = match[1];
88
+ if (
89
+ importPath.startsWith("./") ||
90
+ importPath.startsWith("../") ||
91
+ importPath.startsWith("#")
92
+ ) {
93
+ imports.push(importPath);
94
+ }
95
+ }
96
+
97
+ return imports;
98
+ }
99
+
100
+ async function resolveImportPath(importPath: string, fromFile: string) {
101
+ const fromDir = fromFile.split("/").slice(0, -1).join("/");
102
+
103
+ if (importPath.startsWith("#")) {
104
+ const imports = await getPackageImports();
105
+ for (const [alias, target] of Object.entries(imports)) {
106
+ if (importPath === alias || importPath.startsWith(alias + "/")) {
107
+ const relativePath = importPath.slice(alias.length + 1);
108
+ const baseDir = target;
109
+ const extensions = [".tsx", ".ts", "/index.tsx", "/index.ts"];
110
+ for (const ext of extensions) {
111
+ const fullPath = relativePath ? join(baseDir, relativePath + ext) : baseDir + ext;
112
+ if (await exists(fullPath)) {
113
+ return fullPath;
114
+ }
115
+ }
116
+ const fullPath = relativePath ? join(baseDir, relativePath) : baseDir;
117
+ if (await exists(fullPath)) {
118
+ return fullPath;
119
+ }
120
+ return null;
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ if (importPath.startsWith("./") || importPath.startsWith("../")) {
127
+ const extensions = [".tsx", ".ts", "/index.tsx", "/index.ts", ""];
128
+ for (const ext of extensions) {
129
+ const fullPath = join(fromDir, importPath + ext);
130
+ if (await exists(fullPath)) {
131
+ return fullPath;
132
+ }
133
+ }
134
+ }
135
+
136
+ return null;
137
+ }
138
+
139
+ async function extractAllCssImports(filePath: string, visited: Set<string> = new Set()) {
140
+ if (visited.has(filePath)) {
141
+ return [];
142
+ }
143
+ visited.add(filePath);
144
+
145
+ const allCss: string[] = [];
146
+ const fileDir = filePath.split("/").slice(0, -1).join("/");
147
+
148
+ const cssImports = await extractCssImports(filePath);
149
+ for (const cssImport of cssImports) {
150
+ const cssPath = join(fileDir, cssImport);
151
+ if (cssPath.startsWith(appDir)) {
152
+ allCss.push(cssPath.replace(appDir + "/", "./"));
153
+ } else {
154
+ allCss.push(cssImport);
155
+ }
156
+ }
157
+
158
+ const componentImports = await extractComponentImports(filePath);
159
+ for (const importPath of componentImports) {
160
+ const resolvedPath = await resolveImportPath(importPath, filePath);
161
+ if (resolvedPath && (await exists(resolvedPath))) {
162
+ const nestedCss = await extractAllCssImports(resolvedPath, visited);
163
+ allCss.push(...nestedCss);
164
+ }
165
+ }
166
+
167
+ return allCss;
168
+ }
169
+
170
+ return {
171
+ scanFiles,
172
+ getPackageImports,
173
+ findRouteModules,
174
+ findLayouts,
175
+ findErrorFile,
176
+ findClientComponents,
177
+ extractCssImports,
178
+ extractComponentImports,
179
+ resolveImportPath,
180
+ extractAllCssImports,
181
+ };
182
+ }
package/src/build.ts ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, type ChildProcess } from "node:child_process";
3
+ import { watch } from "node:fs";
4
+ import { writeFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { argv, env } from "node:process";
7
+ import { parseArgs } from "node:util";
8
+ import { buildClient } from "./build.bundle-client.ts";
9
+ import { buildServer } from "./build.bundle-server.ts";
10
+ import { exists } from "./fs.ts";
11
+
12
+ /** Resolve paths relative to the current working directory. */
13
+ const ROOT_DIR = process.cwd();
14
+ const APP_DIR = join(ROOT_DIR, "src");
15
+ const DIST_DIR = join(ROOT_DIR, "dist");
16
+ const DIST_CLIENT = join(DIST_DIR, "client");
17
+ const DIST_SERVER = join(DIST_DIR, "server");
18
+ const PUBLIC_DIR = join(ROOT_DIR, "public");
19
+
20
+ /** Generated file paths. */
21
+ const MODULES_PATH = join(DIST_DIR, ".modules.generated.ts");
22
+ const CHUNKS_PATH = join(DIST_DIR, ".chunks.generated.json");
23
+ const ROUTES_TYPE_PATH = join(DIST_DIR, "routes.d.ts");
24
+
25
+ /** CLI entry point: parse args early so they're available. */
26
+ const { values: args, positionals } = parseArgs({
27
+ allowPositionals: true,
28
+ args: argv.slice(2),
29
+ options: {
30
+ production: {
31
+ type: "boolean",
32
+ short: "p",
33
+ default: env.NODE_ENV === "production",
34
+ },
35
+ serve: { type: "boolean", short: "s", default: false },
36
+ watch: { type: "boolean", short: "w", default: false },
37
+ clean: { type: "boolean", short: "c", default: false },
38
+ debug: { type: "boolean", short: "d", default: false },
39
+ sourcemap: { type: "boolean", default: false },
40
+ codemod: { type: "boolean", default: false },
41
+ dry: { type: "boolean", default: false },
42
+ },
43
+ });
44
+
45
+ /** Auto-scaffolds missing template files. */
46
+ async function scaffoldTemplates() {
47
+ const templates: Record<string, string> = {
48
+ "index.ts": /* tsx */ `import worker from "@chr33s/solarflare";
49
+ export default { fetch: worker };
50
+ `,
51
+ "_error.tsx": /* tsx */ `export default function ErrorPage({ error }: { error: Error }) {
52
+ return <div><h1>Error</h1><p>{error.message}</p></div>;
53
+ }
54
+ `,
55
+ "_layout.tsx": /* tsx */ `import type { VNode } from "preact";
56
+ import { Head, Body } from "@chr33s/solarflare/server";
57
+
58
+ export default function Layout({ children }: { children: VNode }) {
59
+ return <html><head><Head /></head><body>{children}<Body /></body></html>;
60
+ }
61
+ `,
62
+ };
63
+
64
+ const rootTemplates: Record<string, string> = {
65
+ "wrangler.json": /* json */ `
66
+ {
67
+ "assets": { "directory": "./dist/client" },
68
+ "compatibility_date": "2025-12-10",
69
+ "compatibility_flags": ["nodejs_compat"],
70
+ "dev": { "port": 8080 },
71
+ "main": "./dist/server/index.js",
72
+ "name": "solarflare"
73
+ }
74
+ `,
75
+ };
76
+
77
+ for (const [filename, content] of Object.entries(templates)) {
78
+ const filepath = join(APP_DIR, filename);
79
+ if (!(await exists(filepath))) {
80
+ await writeFile(filepath, content);
81
+ }
82
+ }
83
+
84
+ for (const [filename, content] of Object.entries(rootTemplates)) {
85
+ const filepath = join(ROOT_DIR, filename);
86
+ if (!(await exists(filepath))) {
87
+ await writeFile(filepath, content);
88
+ }
89
+ }
90
+ }
91
+
92
+ /** Cleans the dist directory. */
93
+ async function clean() {
94
+ const { rm } = await import("fs/promises");
95
+ try {
96
+ await rm(DIST_DIR, { recursive: true, force: true });
97
+ console.log("🧹 Cleaned dist directory");
98
+ } catch {}
99
+ }
100
+
101
+ /** Main build function. */
102
+ async function build() {
103
+ const startTime = performance.now();
104
+
105
+ console.log("\n⚡ Solarflare Build\n");
106
+
107
+ if (args.clean) {
108
+ await clean();
109
+ }
110
+
111
+ await scaffoldTemplates();
112
+
113
+ await buildClient({
114
+ args,
115
+ rootDir: ROOT_DIR,
116
+ appDir: APP_DIR,
117
+ distDir: DIST_DIR,
118
+ distClient: DIST_CLIENT,
119
+ publicDir: PUBLIC_DIR,
120
+ chunksPath: CHUNKS_PATH,
121
+ });
122
+
123
+ await buildServer({
124
+ args,
125
+ rootDir: ROOT_DIR,
126
+ appDir: APP_DIR,
127
+ distServer: DIST_SERVER,
128
+ modulesPath: MODULES_PATH,
129
+ chunksPath: CHUNKS_PATH,
130
+ routesTypePath: ROUTES_TYPE_PATH,
131
+ });
132
+
133
+ const duration = ((performance.now() - startTime) / 1000).toFixed(2);
134
+ console.log(`\n🚀 Build completed in ${duration}s\n`);
135
+ }
136
+
137
+ /** Watch mode - rebuilds on file changes and optionally starts dev server. */
138
+ async function watchBuild() {
139
+ console.log("\n⚡ Solarflare Dev Mode\n");
140
+
141
+ try {
142
+ await build();
143
+ } catch (err) {
144
+ console.error("❌ Initial build failed:", err);
145
+ }
146
+
147
+ let wranglerProc: ChildProcess | null = null;
148
+
149
+ if (args.serve) {
150
+ console.log("🌐 Starting wrangler dev server...\n");
151
+ wranglerProc = spawn("npx", ["wrangler", "dev"], {
152
+ stdio: "inherit",
153
+ env: { ...env },
154
+ });
155
+ }
156
+
157
+ console.log("\n👀 Watching for changes...\n");
158
+
159
+ let debounceTimer: NodeJS.Timeout | null = null;
160
+ let isBuilding = false;
161
+ let pendingBuild = false;
162
+ const DEBOUNCE_MS = 150;
163
+
164
+ async function doBuild(filename: string) {
165
+ if (isBuilding) {
166
+ pendingBuild = true;
167
+ return;
168
+ }
169
+
170
+ isBuilding = true;
171
+ console.log(`\n🔄 Change detected: ${filename}\n`);
172
+ try {
173
+ await build();
174
+ console.log("\n👀 Watching for changes...\n");
175
+ } catch (err) {
176
+ console.error("❌ Build failed:", err);
177
+ console.log("\n👀 Watching for changes...\n");
178
+ } finally {
179
+ isBuilding = false;
180
+ if (pendingBuild) {
181
+ pendingBuild = false;
182
+ await doBuild("(queued changes)");
183
+ }
184
+ }
185
+ }
186
+
187
+ const watcher = watch(APP_DIR, { recursive: true }, (_event, filename) => {
188
+ if (!filename) return;
189
+
190
+ if (filename.endsWith(".generated.ts") || filename.endsWith(".generated.json")) {
191
+ return;
192
+ }
193
+
194
+ if (debounceTimer) {
195
+ clearTimeout(debounceTimer);
196
+ }
197
+
198
+ debounceTimer = setTimeout(() => {
199
+ void doBuild(filename);
200
+ }, DEBOUNCE_MS);
201
+ });
202
+
203
+ let isExiting = false;
204
+ process.on("SIGINT", () => {
205
+ if (isExiting) return;
206
+ isExiting = true;
207
+ console.log("\n\n👋 Stopping dev server...\n");
208
+ watcher.close();
209
+ if (wranglerProc) {
210
+ wranglerProc.kill("SIGTERM");
211
+ }
212
+ process.exit(0);
213
+ });
214
+
215
+ await new Promise(() => {});
216
+ }
217
+
218
+ if (import.meta.main) {
219
+ if (args.codemod) {
220
+ const { codemod } = await import("./codemod.ts");
221
+ codemod(positionals, args);
222
+ } else if (args.watch) {
223
+ void watchBuild();
224
+ } else {
225
+ void build();
226
+ }
227
+ }
@@ -0,0 +1,63 @@
1
+ import { join } from "node:path";
2
+ import { createProgram, validateModule, type ValidationResult } from "./ast.ts";
3
+ import { parsePath } from "./paths.ts";
4
+
5
+ export async function validateRoutes(routeFiles: string[], layoutFiles: string[], appDir: string) {
6
+ const allFiles = [
7
+ ...routeFiles.map((f) => join(appDir, f)),
8
+ ...layoutFiles.map((f) => join(appDir, f)),
9
+ ];
10
+
11
+ if (allFiles.length === 0) return true;
12
+
13
+ const program = createProgram(allFiles);
14
+ const results: ValidationResult[] = [];
15
+
16
+ for (const file of [...routeFiles, ...layoutFiles]) {
17
+ const result = validateModule(program, file, appDir);
18
+ results.push(result);
19
+ }
20
+
21
+ let hasErrors = false;
22
+ for (const result of results) {
23
+ for (const error of result.errors) {
24
+ console.error(` ❌ ${result.file}: ${error}`);
25
+ hasErrors = true;
26
+ }
27
+ for (const warning of result.warnings) {
28
+ console.warn(` ⚠️ ${result.file}: ${warning}`);
29
+ }
30
+ }
31
+
32
+ return !hasErrors;
33
+ }
34
+
35
+ export function generateRoutesTypeFile(routeFiles: string[]) {
36
+ const clientRoutes = routeFiles.filter((f) => f.includes(".client."));
37
+
38
+ const routeTypes = clientRoutes
39
+ .map((file) => {
40
+ const parsed = parsePath(file);
41
+ const paramsType =
42
+ parsed.params.length > 0
43
+ ? `{ ${parsed.params.map((p) => `${p}: string`).join("; ")} }`
44
+ : "Record<string, never>";
45
+ return ` '${parsed.pattern}': { params: ${paramsType} }`;
46
+ })
47
+ .join("\n");
48
+
49
+ return /* ts */ `
50
+ /**
51
+ * Auto-generated Route Types
52
+ * Provides type-safe route definitions
53
+ */
54
+
55
+ export interface Routes {
56
+ ${routeTypes}
57
+ }
58
+
59
+ export type RoutePath = keyof Routes;
60
+
61
+ export type RouteParams<T extends RoutePath> = Routes[T]['params'];
62
+ `;
63
+ }
@@ -0,0 +1,78 @@
1
+ /** Callback type for HMR events. */
2
+ export type HmrCallback<T = unknown> = (data: T) => void;
3
+
4
+ /** HMR API interface for SSE-based hot module replacement. */
5
+ export interface HmrApi {
6
+ /** Listen to an HMR event. */
7
+ on<T = unknown>(event: string, cb: HmrCallback<T>): void;
8
+ /** Remove an HMR event listener. */
9
+ off<T = unknown>(event: string, cb: HmrCallback<T>): void;
10
+ /** Register cleanup callback before module disposal. */
11
+ dispose(cb: () => void): void;
12
+ /** Persistent data across HMR updates. */
13
+ data: Record<string, unknown>;
14
+ }
15
+
16
+ /** No-op HMR implementation for production - tree-shakes to nothing. */
17
+ const noopHmr: HmrApi = {
18
+ on() {},
19
+ off() {},
20
+ dispose() {},
21
+ data: {},
22
+ };
23
+
24
+ /** Creates the development HMR client with SSE connection. */
25
+ function createDevHmr() {
26
+ const listeners = new Map<string, Set<HmrCallback>>();
27
+ const disposeCallbacks: Array<() => void> = [];
28
+ const data: Record<string, unknown> = {};
29
+
30
+ if (typeof EventSource !== "undefined" && typeof location !== "undefined") {
31
+ const es = new EventSource(`${location.origin}/_hmr`);
32
+
33
+ es.onopen = () => {
34
+ console.log("[HMR] Connected to dev server");
35
+ };
36
+
37
+ es.onmessage = (e) => {
38
+ try {
39
+ const { type, ...payload } = JSON.parse(e.data);
40
+ const cbs = listeners.get(type);
41
+ if (cbs) {
42
+ for (const cb of cbs) {
43
+ try {
44
+ cb(payload);
45
+ } catch (err) {
46
+ console.error(`[HMR] Error in handler for ${type}:`, err);
47
+ }
48
+ }
49
+ }
50
+ } catch (err) {
51
+ console.error("[HMR] Failed to parse message:", err);
52
+ }
53
+ };
54
+
55
+ es.onerror = () => {
56
+ console.log("[HMR] Connection lost, reconnecting...");
57
+ };
58
+ }
59
+
60
+ return {
61
+ on<T = unknown>(event: string, cb: HmrCallback<T>) {
62
+ if (!listeners.has(event)) listeners.set(event, new Set());
63
+ listeners.get(event)!.add(cb as HmrCallback);
64
+ },
65
+ off<T = unknown>(event: string, cb: HmrCallback<T>) {
66
+ listeners.get(event)?.delete(cb as HmrCallback);
67
+ },
68
+ dispose(cb: () => void) {
69
+ disposeCallbacks.push(cb);
70
+ },
71
+ data,
72
+ };
73
+ }
74
+
75
+ /** HMR instance - dev in dev mode, no-op in production. */
76
+ export const hmr: HmrApi = (globalThis as unknown as { __SF_DEV__?: boolean }).__SF_DEV__
77
+ ? createDevHmr()
78
+ : noopHmr;
@@ -0,0 +1,68 @@
1
+ import { stylesheets, supportsConstructableStylesheets } from "./stylesheets.ts";
2
+
3
+ /** Style loading state for a component. */
4
+ interface StyleState {
5
+ loaded: boolean;
6
+ sheets: CSSStyleSheet[];
7
+ }
8
+
9
+ /** Component style states. */
10
+ const componentStyles = new Map<string, StyleState>();
11
+
12
+ /** Loads and applies styles for a component. */
13
+ export async function loadComponentStyles(tag: string, cssUrls: string[]) {
14
+ const existing = componentStyles.get(tag);
15
+ if (existing?.loaded) {
16
+ return existing.sheets;
17
+ }
18
+
19
+ const sheets: CSSStyleSheet[] = [];
20
+
21
+ for (const url of cssUrls) {
22
+ let sheet = stylesheets.get(url);
23
+
24
+ if (!sheet) {
25
+ try {
26
+ const response = await fetch(url);
27
+ const css = await response.text();
28
+ sheet = stylesheets.register(url, css, { consumer: tag });
29
+ } catch (e) {
30
+ console.warn(`[styles] Failed to load ${url}:`, e);
31
+ continue;
32
+ }
33
+ }
34
+
35
+ if (sheet) {
36
+ sheets.push(sheet);
37
+ }
38
+ }
39
+
40
+ componentStyles.set(tag, { loaded: true, sheets });
41
+ return sheets;
42
+ }
43
+
44
+ /** Applies stylesheets to a custom element. */
45
+ export function applyStyles(element: HTMLElement, sheets: CSSStyleSheet[]) {
46
+ if (!supportsConstructableStylesheets()) {
47
+ // Fallback handled by StylesheetManager
48
+ return;
49
+ }
50
+
51
+ const shadowRoot = element.shadowRoot;
52
+
53
+ if (shadowRoot) {
54
+ shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, ...sheets];
55
+ } else {
56
+ for (const sheet of sheets) {
57
+ if (!document.adoptedStyleSheets.includes(sheet)) {
58
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ /** Cleans up styles when a component is disconnected. */
65
+ export function cleanupStyles(tag: string) {
66
+ stylesheets.removeConsumer(tag);
67
+ componentStyles.delete(tag);
68
+ }