@solidjs/start 2.0.0-alpha.3 → 2.0.0-beta.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @solidjs/start
2
2
 
3
+ ## 2.0.0-beta.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0a9fdc3: fix cloudflare
8
+
9
+ ### Patch Changes
10
+
11
+ - 0c95804: Fix URL to path conversion in manifest resolver
12
+ - 6011e5a: Fixed changes in route files resulting in a reload instead of hot module replace. Reloads now only are triggered when adding or removing routes.
13
+ - 8fb81e6: Fixed niche edge cases in the server functions dead code removal (DCE) logic:
14
+ - Server functions only referenced in event handlers (e.g. `onClick`) now aren't considered unused and work properly.
15
+ - Unused variables in server functions no longer lead to compilation errors.
16
+ - 8d12d6a: Fixed tailwind class changes not getting applied via HMR in filesystem routes.
17
+ - ec1b82b: Included Vite 8 in the peer dependency range.
18
+
3
19
  ## 2.0.0-alpha.3
4
20
 
5
21
  ### Minor Changes
@@ -11,6 +11,6 @@ export declare const VIRTUAL_MODULES: {
11
11
  readonly app: "solid-start:app";
12
12
  };
13
13
  export declare const VITE_ENVIRONMENTS: {
14
- client: string;
15
- server: string;
14
+ client: "client";
15
+ server: "ssr";
16
16
  };
@@ -1,3 +1,4 @@
1
+ import { debounce } from "../../utils/debounce.js";
1
2
  import { VITE_ENVIRONMENTS } from "../constants.js";
2
3
  import { moduleId } from "./index.js";
3
4
  function setupWatcher(watcher, routes) {
@@ -6,37 +7,46 @@ function setupWatcher(watcher, routes) {
6
7
  watcher.on("change", path => routes.updateRoute(path));
7
8
  }
8
9
  function createRoutesReloader(server, routes, environment) {
9
- routes.addEventListener("reload", handleRoutesReload);
10
- return () => routes.removeEventListener("reload", handleRoutesReload);
11
- function handleRoutesReload() {
12
- const envName = environment === "ssr" ? VITE_ENVIRONMENTS.server : VITE_ENVIRONMENTS.client;
13
- const devEnv = server.environments[envName];
14
- if (!devEnv?.moduleGraph)
15
- return;
10
+ const devEnv = server.environments[environment];
11
+ if (!devEnv?.moduleGraph)
12
+ return;
13
+ /**
14
+ * Debounce catches multiple route changes in a row
15
+ * Short timeout for inexpensive invalidations
16
+ */
17
+ const invalidateModule = debounce((mod) => {
18
+ devEnv.moduleGraph.invalidateModule(mod);
19
+ }, 0);
20
+ /**
21
+ * Long debounce timeout for expensive reloads
22
+ */
23
+ const reloadModule = debounce((mod) => {
24
+ devEnv.reloadModule(mod);
25
+ }, 200);
26
+ return routes.on("reload", function handleRoutesReload(evt) {
16
27
  const mod = devEnv.moduleGraph.getModuleById(moduleId);
17
- if (mod) {
18
- const seen = new Set();
19
- devEnv.moduleGraph.invalidateModule(mod, seen);
28
+ if (!mod) {
29
+ devEnv.hot.send({ type: "full-reload" });
30
+ return;
20
31
  }
21
- if (environment !== "ssr") {
22
- if (mod) {
23
- devEnv.reloadModule(mod);
24
- }
25
- else if (devEnv.hot) {
26
- devEnv.hot.send({ type: "full-reload" });
27
- }
32
+ if (environment === VITE_ENVIRONMENTS.client && evt.detail.type !== "update") {
33
+ // Client has to be reloaded when routes are added or removed
34
+ reloadModule(mod);
35
+ }
36
+ else {
37
+ invalidateModule(mod);
28
38
  }
29
- }
39
+ });
30
40
  }
31
41
  export const fileSystemWatcher = (routers) => {
32
42
  const plugin = {
33
43
  name: "fs-watcher",
34
44
  async configureServer(server) {
35
- Object.keys(routers).forEach(environment => {
36
- const router = globalThis.ROUTERS[environment];
45
+ for (const environment of [VITE_ENVIRONMENTS.server, VITE_ENVIRONMENTS.client]) {
46
+ const router = routers[environment];
37
47
  setupWatcher(server.watcher, router);
38
48
  createRoutesReloader(server, router, environment);
39
- });
49
+ }
40
50
  },
41
51
  };
42
52
  return plugin;
@@ -10,6 +10,10 @@ type Route = {
10
10
  } & Record<string, any>;
11
11
  export declare function cleanPath(src: string, config: FileSystemRouterConfig): string;
12
12
  export declare function analyzeModule(src: string): readonly [imports: readonly import("es-module-lexer").ImportSpecifier[], exports: readonly import("es-module-lexer").ExportSpecifier[], facade: boolean, hasModuleSyntax: boolean];
13
+ type RouterEvent = CustomEvent<{
14
+ route: string;
15
+ type: "update" | "remove" | "add";
16
+ }>;
13
17
  export declare class BaseFileSystemRouter extends EventTarget {
14
18
  routes: Route[];
15
19
  config: FileSystemRouterConfig;
@@ -27,11 +31,12 @@ export declare class BaseFileSystemRouter extends EventTarget {
27
31
  * To be attached by vite plugin to the vite dev server
28
32
  */
29
33
  update: undefined;
30
- _addRoute(route: Route): void;
34
+ _addRoute(route: Route): boolean;
31
35
  addRoute(src: string): Promise<void>;
32
- reload(route: string): void;
33
- updateRoute(src: string): Promise<void>;
36
+ reload(route: string, type: "update" | "remove" | "add"): void;
37
+ updateRoute(src_: string): Promise<void>;
34
38
  removeRoute(src: string): void;
39
+ on(type: string, cb: (evt: RouterEvent) => void): () => void;
35
40
  buildRoutesPromise?: Promise<any[]>;
36
41
  getRoutes(): Promise<Route[]>;
37
42
  }
@@ -71,8 +71,11 @@ export class BaseFileSystemRouter extends EventTarget {
71
71
  */
72
72
  update = undefined;
73
73
  _addRoute(route) {
74
- this.routes = this.routes.filter(r => r.path !== route.path);
74
+ const idx = this.routes.findIndex(r => r.path === route.path);
75
+ if (idx >= 0)
76
+ this.routes.splice(idx, 1);
75
77
  this.routes.push(route);
78
+ return idx >= 0;
76
79
  }
77
80
  async addRoute(src) {
78
81
  src = normalizePath(src);
@@ -81,7 +84,7 @@ export class BaseFileSystemRouter extends EventTarget {
81
84
  const route = this.toRoute(src);
82
85
  if (route) {
83
86
  this._addRoute(route);
84
- this.reload(route.path);
87
+ this.reload(route.path, "add");
85
88
  }
86
89
  }
87
90
  catch (e) {
@@ -89,22 +92,25 @@ export class BaseFileSystemRouter extends EventTarget {
89
92
  }
90
93
  }
91
94
  }
92
- reload(route) {
93
- this.dispatchEvent(new Event("reload", {
94
- // @ts-ignore
95
+ reload(route, type) {
96
+ this.dispatchEvent(new CustomEvent("reload", {
95
97
  detail: {
96
98
  route,
99
+ type,
97
100
  },
98
101
  }));
99
102
  }
100
- async updateRoute(src) {
101
- src = normalizePath(src);
103
+ async updateRoute(src_) {
104
+ const src = normalizePath(src_);
102
105
  if (this.isRoute(src)) {
103
106
  try {
104
107
  const route = this.toRoute(src);
105
108
  if (route) {
106
- this._addRoute(route);
107
- this.reload(route.path);
109
+ const updated = this._addRoute(route);
110
+ this.reload(route.path, updated ? "update" : "add");
111
+ }
112
+ else {
113
+ this.removeRoute(src_);
108
114
  }
109
115
  }
110
116
  catch (e) {
@@ -120,10 +126,17 @@ export class BaseFileSystemRouter extends EventTarget {
120
126
  if (path === undefined) {
121
127
  return;
122
128
  }
123
- this.routes = this.routes.filter(r => r.path !== path);
124
- this.dispatchEvent(new Event("reload", {}));
129
+ const idx = this.routes.findIndex(r => r.path === path);
130
+ if (idx === -1)
131
+ return;
132
+ this.routes.splice(idx, 1);
133
+ this.reload(path, "remove");
125
134
  }
126
135
  }
136
+ on(type, cb) {
137
+ this.addEventListener(type, cb);
138
+ return () => this.removeEventListener(type, cb);
139
+ }
127
140
  buildRoutesPromise;
128
141
  async getRoutes() {
129
142
  if (!this.buildRoutesPromise) {
@@ -260,9 +260,6 @@ function treeShakeTransform({ types: t }) {
260
260
  };
261
261
  }
262
262
  export function treeShake() {
263
- let config;
264
- const cache = {};
265
- let server;
266
263
  async function transform(id, code) {
267
264
  const [path, queryString] = id.split("?");
268
265
  const query = new URLSearchParams(queryString);
@@ -286,44 +283,6 @@ export function treeShake() {
286
283
  return {
287
284
  name: "tree-shake",
288
285
  enforce: "pre",
289
- configResolved(resolvedConfig) {
290
- config = resolvedConfig;
291
- },
292
- configureServer(s) {
293
- server = s;
294
- },
295
- async handleHotUpdate(ctx) {
296
- if (cache[ctx.file]) {
297
- const mods = [];
298
- const newCode = await ctx.read();
299
- for (const [id, code] of Object.entries(cache[ctx.file])) {
300
- const transformed = await transform(id, newCode);
301
- if (!transformed)
302
- continue;
303
- const { code: transformedCode } = transformed;
304
- if (transformedCode !== code) {
305
- const mod = server.moduleGraph.getModuleById(id);
306
- if (mod)
307
- mods.push(mod);
308
- }
309
- cache[ctx.file] ??= {};
310
- cache[ctx.file][id] = transformedCode;
311
- // server.moduleGraph.setModuleSource(id, code);
312
- }
313
- return mods;
314
- }
315
- // const mods = [];
316
- // [...server.moduleGraph.urlToModuleMap.entries()].forEach(([url, m]) => {
317
- // if (m.file === ctx.file && m.id.includes("pick=")) {
318
- // if (!m.id.includes("pick=loader")) {
319
- // mods.push(m);
320
- // }
321
- // }
322
- // });
323
- // return mods;
324
- // // this.router.updateRoute(ctx.path);
325
- // }
326
- },
327
286
  async transform(code, id) {
328
287
  const [path, queryString] = id.split("?");
329
288
  if (!path)
@@ -336,8 +295,6 @@ export function treeShake() {
336
295
  const transformed = await transform(id, code);
337
296
  if (!transformed?.code)
338
297
  return;
339
- cache[path] ??= {};
340
- cache[path][id] = transformed.code;
341
298
  return {
342
299
  code: transformed.code,
343
300
  map: transformed.map,
@@ -37,6 +37,18 @@ export function solidStart(options) {
37
37
  server: `${start.appRoot}/entry-server${entryExtension}`,
38
38
  };
39
39
  return [
40
+ // TODO (Alexis): check if the comment below is still relevant
41
+ //
42
+ // Must be placed after fsRoutes, as treeShake will remove the
43
+ // server fn exports added in by this plugin
44
+ serverFunctionsPlugin({
45
+ manifest: VIRTUAL_MODULES.serverFnManifest,
46
+ runtime: {
47
+ server: '@solidjs/start/fns/server',
48
+ client: '@solidjs/start/fns/client',
49
+ },
50
+ filter: options?.serverFunctions?.filter,
51
+ }),
40
52
  {
41
53
  name: "solid-start:config",
42
54
  enforce: "pre",
@@ -166,16 +178,6 @@ export function solidStart(options) {
166
178
  }),
167
179
  lazy(),
168
180
  envPlugin(options?.env),
169
- // Must be placed after fsRoutes, as treeShake will remove the
170
- // server fn exports added in by this plugin
171
- serverFunctionsPlugin({
172
- manifest: VIRTUAL_MODULES.serverFnManifest,
173
- runtime: {
174
- server: '@solidjs/start/fns/server',
175
- client: '@solidjs/start/fns/client',
176
- },
177
- filter: options?.serverFunctions?.filter,
178
- }),
179
181
  {
180
182
  name: "solid-start:virtual-modules",
181
183
  async resolveId(id) {
@@ -77,6 +77,8 @@ const lazy = () => {
77
77
  // Therefore we skip, if the src doesn't have any import
78
78
  if (src.indexOf("import") === -1)
79
79
  return;
80
+ if (id.includes("entry-server"))
81
+ return;
80
82
  const plugins = [];
81
83
  const hasDefaultExport = src.indexOf("export default") !== -1;
82
84
  if (hasDefaultExport) {
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { version as viteVersion } from "vite";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { findStylesInModuleGraph } from "../server/collect-styles.js";
5
6
  import { VIRTUAL_MODULES } from "./constants.js";
6
7
  import { wrapId } from "./vite-utils.js";
@@ -16,11 +17,11 @@ export function manifest(start) {
16
17
  if (id === VIRTUAL_MODULES.clientViteManifest)
17
18
  return `\0${VIRTUAL_MODULES.clientViteManifest}`;
18
19
  if (id === VIRTUAL_MODULES.getClientManifest)
19
- return this.resolve(new URL("../server/manifest/client-manifest", import.meta.url).pathname);
20
+ return this.resolve(fileURLToPath(new URL("../server/manifest/client-manifest", import.meta.url)));
20
21
  if (id === VIRTUAL_MODULES.getManifest) {
21
22
  return this.environment.config.consumer === "client"
22
- ? this.resolve(new URL("../server/manifest/client-manifest", import.meta.url).pathname)
23
- : this.resolve(new URL("../server/manifest/ssr-manifest", import.meta.url).pathname);
23
+ ? this.resolve(fileURLToPath(new URL("../server/manifest/client-manifest", import.meta.url)))
24
+ : this.resolve(fileURLToPath(new URL("../server/manifest/ssr-manifest", import.meta.url)));
24
25
  }
25
26
  if (id === VIRTUAL_MODULES.middleware) {
26
27
  if (start.middleware)
@@ -140,6 +140,7 @@ export function serverFunctionsPlugin(options) {
140
140
  },
141
141
  {
142
142
  name: "solid-start:server-functions/compiler",
143
+ enforce: 'pre',
143
144
  async transform(code, fileId, opts) {
144
145
  const mode = opts?.ssr ? "server" : "client";
145
146
  const [id] = fileId.split("?");
@@ -1,5 +1,17 @@
1
1
  import * as t from "@babel/types";
2
2
  import { isPathValid } from "./paths.js";
3
+ function isInvalidForRemoval(path) {
4
+ if (isPathValid(path, t.isCatchClause)) {
5
+ // This case is for `catch (error)` blocks
6
+ return true;
7
+ }
8
+ // This one is for destructured variables
9
+ let target = path;
10
+ if (isPathValid(path, t.isVariableDeclarator)) {
11
+ target = path.get('id');
12
+ }
13
+ return isPathValid(target, t.isObjectPattern) || isPathValid(target, t.isArrayPattern);
14
+ }
3
15
  export function removeUnusedVariables(program) {
4
16
  // TODO(Alexis):
5
17
  // This implementation is simple but slow
@@ -20,19 +32,20 @@ export function removeUnusedVariables(program) {
20
32
  case "hoisted":
21
33
  case "module":
22
34
  if (binding.references === 0 && !binding.path.removed) {
23
- if (isPathValid(binding.path.parentPath, t.isImportDeclaration)) {
24
- const parent = binding.path.parentPath;
35
+ const parent = binding.path.parentPath;
36
+ if (isPathValid(parent, t.isImportDeclaration)) {
25
37
  if (parent.node.specifiers.length === 1) {
26
38
  parent.remove();
27
39
  }
28
40
  else {
29
41
  binding.path.remove();
30
42
  }
43
+ dirty = true;
31
44
  }
32
- else {
45
+ else if (!(isInvalidForRemoval(binding.path))) {
33
46
  binding.path.remove();
47
+ dirty = true;
34
48
  }
35
- dirty = true;
36
49
  }
37
50
  break;
38
51
  case "local":
@@ -1,4 +1,4 @@
1
- import { type H3Event } from "h3";
1
+ import { type H3Event } from "h3/generic";
2
2
  import type { JSX } from "solid-js";
3
3
  import type { FetchEvent, HandlerOptions, PageEvent } from "./types.ts";
4
4
  export declare function createBaseHandler(createPageEvent: (e: FetchEvent) => Promise<PageEvent>, fn: (context: PageEvent) => JSX.Element, options?: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise<HandlerOptions>)): {
@@ -1,5 +1,5 @@
1
1
  import middleware from "solid-start:middleware";
2
- import { defineHandler, getCookie, H3, redirect, setCookie } from "h3";
2
+ import { defineHandler, getCookie, H3, redirect, setCookie } from "h3/generic";
3
3
  import { sharedConfig } from "solid-js";
4
4
  import { getRequestEvent, renderToStream, renderToString } from "solid-js/web";
5
5
  import { createRoutes } from "../router.jsx";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Creates a debounced function that delays the invocation of a function
3
+ */
4
+ export declare const debounce: <T extends (...args: any[]) => void>(cb: T, debounceMs: number) => (...args: Parameters<T>) => void;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Creates a debounced function that delays the invocation of a function
3
+ */
4
+ export const debounce = (cb, debounceMs) => {
5
+ let timeout;
6
+ return (...args) => {
7
+ clearTimeout(timeout);
8
+ timeout = setTimeout(() => cb(...args), debounceMs);
9
+ };
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/start",
3
- "version": "2.0.0-alpha.3",
3
+ "version": "2.0.0-beta.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -62,7 +62,7 @@
62
62
  "vitest": "^4.1.0"
63
63
  },
64
64
  "peerDependencies": {
65
- "vite": "^7"
65
+ "vite": "^7 || ^8"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "pnpm validate-imports && tsc && node scripts/build.js",
@@ -14,6 +14,6 @@ export const VIRTUAL_MODULES = {
14
14
  } as const;
15
15
 
16
16
  export const VITE_ENVIRONMENTS = {
17
- client: "client",
18
- server: "ssr",
17
+ client: "client" as const,
18
+ server: "ssr" as const,
19
19
  };
@@ -1,22 +1,10 @@
1
- import type {
2
- EnvironmentModuleNode,
3
- FSWatcher,
4
- PluginOption,
5
- ViteDevServer,
6
- } from "vite";
1
+ import type { EnvironmentModuleNode, FSWatcher, PluginOption, ViteDevServer } from "vite";
2
+ import { debounce } from "../../utils/debounce.ts";
7
3
  import { VITE_ENVIRONMENTS } from "../constants.ts";
8
4
  import { moduleId } from "./index.ts";
9
5
  import type { BaseFileSystemRouter } from "./router.ts";
10
6
 
11
- interface CompiledRouter {
12
- removeRoute(path: string): void;
13
- addRoute(path: string): void;
14
- updateRoute(path: string): void;
15
- addEventListener(event: "reload", handler: () => void): void;
16
- removeEventListener(event: "reload", handler: () => void): void;
17
- }
18
-
19
- function setupWatcher(watcher: FSWatcher, routes: CompiledRouter): void {
7
+ function setupWatcher(watcher: FSWatcher, routes: BaseFileSystemRouter): void {
20
8
  watcher.on("unlink", path => routes.removeRoute(path));
21
9
  watcher.on("add", path => routes.addRoute(path));
22
10
  watcher.on("change", path => routes.updateRoute(path));
@@ -24,33 +12,41 @@ function setupWatcher(watcher: FSWatcher, routes: CompiledRouter): void {
24
12
 
25
13
  function createRoutesReloader(
26
14
  server: ViteDevServer,
27
- routes: CompiledRouter,
15
+ routes: BaseFileSystemRouter,
28
16
  environment: "client" | "ssr",
29
- ): () => void {
30
- routes.addEventListener("reload", handleRoutesReload);
31
- return () => routes.removeEventListener("reload", handleRoutesReload);
17
+ ) {
18
+ const devEnv = server.environments[environment]!;
19
+ if (!devEnv?.moduleGraph) return;
20
+
21
+ /**
22
+ * Debounce catches multiple route changes in a row
23
+ * Short timeout for inexpensive invalidations
24
+ */
25
+ const invalidateModule = debounce((mod: EnvironmentModuleNode) => {
26
+ devEnv.moduleGraph.invalidateModule(mod);
27
+ }, 0);
32
28
 
33
- function handleRoutesReload(): void {
34
- const envName =
35
- environment === "ssr" ? VITE_ENVIRONMENTS.server : VITE_ENVIRONMENTS.client;
36
- const devEnv = server.environments[envName];
37
- if (!devEnv?.moduleGraph) return;
29
+ /**
30
+ * Long debounce timeout for expensive reloads
31
+ */
32
+ const reloadModule = debounce((mod: EnvironmentModuleNode) => {
33
+ devEnv.reloadModule(mod);
34
+ }, 200);
38
35
 
39
- const mod: EnvironmentModuleNode | undefined =
40
- devEnv.moduleGraph.getModuleById(moduleId);
41
- if (mod) {
42
- const seen = new Set<EnvironmentModuleNode>();
43
- devEnv.moduleGraph.invalidateModule(mod, seen);
36
+ return routes.on("reload", function handleRoutesReload(evt): void {
37
+ const mod = devEnv.moduleGraph.getModuleById(moduleId)!;
38
+ if (!mod) {
39
+ devEnv.hot.send({ type: "full-reload" });
40
+ return;
44
41
  }
45
42
 
46
- if (environment !== "ssr") {
47
- if (mod) {
48
- devEnv.reloadModule(mod);
49
- } else if (devEnv.hot) {
50
- devEnv.hot.send({ type: "full-reload" });
51
- }
43
+ if (environment === VITE_ENVIRONMENTS.client && evt.detail.type !== "update") {
44
+ // Client has to be reloaded when routes are added or removed
45
+ reloadModule(mod);
46
+ } else {
47
+ invalidateModule(mod);
52
48
  }
53
- }
49
+ });
54
50
  }
55
51
 
56
52
  export const fileSystemWatcher = (
@@ -59,11 +55,11 @@ export const fileSystemWatcher = (
59
55
  const plugin: PluginOption = {
60
56
  name: "fs-watcher",
61
57
  async configureServer(server: ViteDevServer) {
62
- Object.keys(routers).forEach(environment => {
63
- const router = (globalThis as any).ROUTERS[environment];
58
+ for (const environment of [VITE_ENVIRONMENTS.server, VITE_ENVIRONMENTS.client]) {
59
+ const router = routers[environment];
64
60
  setupWatcher(server.watcher, router);
65
- createRoutesReloader(server, router, environment as keyof typeof routers);
66
- });
61
+ createRoutesReloader(server, router, environment);
62
+ }
67
63
  },
68
64
  };
69
65
  return plugin;
@@ -32,6 +32,8 @@ export function analyzeModule(src: string) {
32
32
  );
33
33
  }
34
34
 
35
+ type RouterEvent = CustomEvent<{ route: string; type: "update" | "remove" | "add" }>;
36
+
35
37
  export class BaseFileSystemRouter extends EventTarget {
36
38
  routes: Route[];
37
39
 
@@ -98,8 +100,11 @@ export class BaseFileSystemRouter extends EventTarget {
98
100
  update = undefined;
99
101
 
100
102
  _addRoute(route: Route) {
101
- this.routes = this.routes.filter(r => r.path !== route.path);
103
+ const idx = this.routes.findIndex(r => r.path === route.path);
104
+ if (idx >= 0) this.routes.splice(idx, 1);
102
105
  this.routes.push(route);
106
+
107
+ return idx >= 0;
103
108
  }
104
109
 
105
110
  async addRoute(src: string) {
@@ -109,7 +114,7 @@ export class BaseFileSystemRouter extends EventTarget {
109
114
  const route = this.toRoute(src);
110
115
  if (route) {
111
116
  this._addRoute(route);
112
- this.reload(route.path);
117
+ this.reload(route.path, "add");
113
118
  }
114
119
  } catch (e) {
115
120
  console.error(e);
@@ -117,25 +122,27 @@ export class BaseFileSystemRouter extends EventTarget {
117
122
  }
118
123
  }
119
124
 
120
- reload(route: string) {
125
+ reload(route: string, type: "update" | "remove" | "add") {
121
126
  this.dispatchEvent(
122
- new Event("reload", {
123
- // @ts-ignore
127
+ new CustomEvent("reload", {
124
128
  detail: {
125
129
  route,
130
+ type,
126
131
  },
127
132
  }),
128
133
  );
129
134
  }
130
135
 
131
- async updateRoute(src: string) {
132
- src = normalizePath(src);
136
+ async updateRoute(src_: string) {
137
+ const src = normalizePath(src_);
133
138
  if (this.isRoute(src)) {
134
139
  try {
135
140
  const route = this.toRoute(src);
136
141
  if (route) {
137
- this._addRoute(route);
138
- this.reload(route.path);
142
+ const updated = this._addRoute(route);
143
+ this.reload(route.path, updated ? "update" : "add");
144
+ } else {
145
+ this.removeRoute(src_);
139
146
  }
140
147
  } catch (e) {
141
148
  console.error(e);
@@ -151,11 +158,20 @@ export class BaseFileSystemRouter extends EventTarget {
151
158
  if (path === undefined) {
152
159
  return;
153
160
  }
154
- this.routes = this.routes.filter(r => r.path !== path);
155
- this.dispatchEvent(new Event("reload", {}));
161
+
162
+ const idx = this.routes.findIndex(r => r.path === path);
163
+ if (idx === -1) return;
164
+
165
+ this.routes.splice(idx, 1);
166
+ this.reload(path, "remove");
156
167
  }
157
168
  }
158
169
 
170
+ on(type: string, cb: (evt: RouterEvent) => void) {
171
+ this.addEventListener(type, cb as any);
172
+ return () => this.removeEventListener(type, cb as any);
173
+ }
174
+
159
175
  buildRoutesPromise?: Promise<any[]>;
160
176
 
161
177
  async getRoutes() {
@@ -6,7 +6,7 @@ import type * as Babel from "@babel/core";
6
6
  import type { NodePath, PluginObj, PluginPass } from "@babel/core";
7
7
  import type { Binding } from "@babel/traverse";
8
8
  import { basename } from "pathe";
9
- import type { Plugin, ResolvedConfig, ViteDevServer } from "vite";
9
+ import type { Plugin } from "vite";
10
10
 
11
11
  type State = Omit<PluginPass, "opts"> & {
12
12
  opts: { pick: string[] };
@@ -280,10 +280,6 @@ function treeShakeTransform({ types: t }: typeof Babel): PluginObj<State> {
280
280
  }
281
281
 
282
282
  export function treeShake(): Plugin {
283
- let config: ResolvedConfig;
284
- const cache: Record<string, any> = {};
285
- let server: ViteDevServer;
286
-
287
283
  async function transform(id: string, code: string) {
288
284
  const [path, queryString] = id.split("?");
289
285
  const query = new URLSearchParams(queryString);
@@ -308,46 +304,6 @@ export function treeShake(): Plugin {
308
304
  return {
309
305
  name: "tree-shake",
310
306
  enforce: "pre",
311
- configResolved(resolvedConfig) {
312
- config = resolvedConfig;
313
- },
314
- configureServer(s) {
315
- server = s;
316
- },
317
- async handleHotUpdate(ctx) {
318
- if (cache[ctx.file]) {
319
- const mods = [];
320
- const newCode = await ctx.read();
321
- for (const [id, code] of Object.entries(cache[ctx.file])) {
322
- const transformed = await transform(id, newCode);
323
- if (!transformed) continue;
324
-
325
- const { code: transformedCode } = transformed;
326
-
327
- if (transformedCode !== code) {
328
- const mod = server.moduleGraph.getModuleById(id);
329
- if (mod) mods.push(mod);
330
- }
331
-
332
- cache[ctx.file] ??= {};
333
- cache[ctx.file][id] = transformedCode;
334
- // server.moduleGraph.setModuleSource(id, code);
335
- }
336
-
337
- return mods;
338
- }
339
- // const mods = [];
340
- // [...server.moduleGraph.urlToModuleMap.entries()].forEach(([url, m]) => {
341
- // if (m.file === ctx.file && m.id.includes("pick=")) {
342
- // if (!m.id.includes("pick=loader")) {
343
- // mods.push(m);
344
- // }
345
- // }
346
- // });
347
- // return mods;
348
- // // this.router.updateRoute(ctx.path);
349
- // }
350
- },
351
307
  async transform(code, id) {
352
308
  const [path, queryString] = id.split("?");
353
309
  if (!path) return;
@@ -358,9 +314,6 @@ export function treeShake(): Plugin {
358
314
  const transformed = await transform(id, code);
359
315
  if (!transformed?.code) return;
360
316
 
361
- cache[path] ??= {};
362
- cache[path][id] = transformed.code;
363
-
364
317
  return {
365
318
  code: transformed.code,
366
319
  map: transformed.map,
@@ -63,6 +63,18 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
63
63
  server: `${start.appRoot}/entry-server${entryExtension}`,
64
64
  };
65
65
  return [
66
+ // TODO (Alexis): check if the comment below is still relevant
67
+ //
68
+ // Must be placed after fsRoutes, as treeShake will remove the
69
+ // server fn exports added in by this plugin
70
+ serverFunctionsPlugin({
71
+ manifest: VIRTUAL_MODULES.serverFnManifest,
72
+ runtime: {
73
+ server: '@solidjs/start/fns/server',
74
+ client: '@solidjs/start/fns/client',
75
+ },
76
+ filter: options?.serverFunctions?.filter,
77
+ }),
66
78
  {
67
79
  name: "solid-start:config",
68
80
  enforce: "pre",
@@ -190,16 +202,6 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
190
202
  }),
191
203
  lazy(),
192
204
  envPlugin(options?.env),
193
- // Must be placed after fsRoutes, as treeShake will remove the
194
- // server fn exports added in by this plugin
195
- serverFunctionsPlugin({
196
- manifest: VIRTUAL_MODULES.serverFnManifest,
197
- runtime: {
198
- server: '@solidjs/start/fns/server',
199
- client: '@solidjs/start/fns/client',
200
- },
201
- filter: options?.serverFunctions?.filter,
202
- }),
203
205
  {
204
206
  name: "solid-start:virtual-modules",
205
207
  async resolveId(id) {
@@ -90,6 +90,7 @@ const lazy = (): PluginOption => {
90
90
  // The transformed files either import "lazy" or css files
91
91
  // Therefore we skip, if the src doesn't have any import
92
92
  if (src.indexOf("import") === -1) return;
93
+ if (id.includes("entry-server")) return;
93
94
 
94
95
  const plugins: PluginItem[] = [];
95
96
 
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { type PluginOption, type ViteDevServer, version as viteVersion } from "vite";
4
+ import { fileURLToPath } from "node:url";
4
5
 
5
6
  import { findStylesInModuleGraph } from "../server/collect-styles.ts";
6
7
  import { VIRTUAL_MODULES } from "./constants.ts";
@@ -20,12 +21,16 @@ export function manifest(start: SolidStartOptions): PluginOption {
20
21
  return `\0${VIRTUAL_MODULES.clientViteManifest}`;
21
22
  if (id === VIRTUAL_MODULES.getClientManifest)
22
23
  return this.resolve(
23
- new URL("../server/manifest/client-manifest", import.meta.url).pathname,
24
+ fileURLToPath(new URL("../server/manifest/client-manifest", import.meta.url)),
24
25
  );
25
26
  if (id === VIRTUAL_MODULES.getManifest) {
26
27
  return this.environment.config.consumer === "client"
27
- ? this.resolve(new URL("../server/manifest/client-manifest", import.meta.url).pathname)
28
- : this.resolve(new URL("../server/manifest/ssr-manifest", import.meta.url).pathname);
28
+ ? this.resolve(
29
+ fileURLToPath(new URL("../server/manifest/client-manifest", import.meta.url)),
30
+ )
31
+ : this.resolve(
32
+ fileURLToPath(new URL("../server/manifest/ssr-manifest", import.meta.url)),
33
+ );
29
34
  }
30
35
  if (id === VIRTUAL_MODULES.middleware) {
31
36
  if (start.middleware) return await this.resolve(start.middleware);
@@ -196,6 +196,7 @@ export function serverFunctionsPlugin(options: ServerFunctionsOptions): Plugin[]
196
196
  },
197
197
  {
198
198
  name: "solid-start:server-functions/compiler",
199
+ enforce: 'pre',
199
200
  async transform(code, fileId, opts) {
200
201
  const mode = opts?.ssr ? "server" : "client";
201
202
  const [id] = fileId.split("?");
@@ -2,6 +2,20 @@ import type * as babel from "@babel/core";
2
2
  import * as t from "@babel/types";
3
3
  import { isPathValid } from "./paths.ts";
4
4
 
5
+ function isInvalidForRemoval(path: babel.NodePath) {
6
+ if (isPathValid(path, t.isCatchClause)) {
7
+ // This case is for `catch (error)` blocks
8
+ return true;
9
+ }
10
+
11
+ // This one is for destructured variables
12
+ let target = path;
13
+ if (isPathValid(path, t.isVariableDeclarator)) {
14
+ target = path.get('id');
15
+ }
16
+ return isPathValid(target, t.isObjectPattern) || isPathValid(target, t.isArrayPattern);
17
+ }
18
+
5
19
  export function removeUnusedVariables(program: babel.NodePath<t.Program>) {
6
20
  // TODO(Alexis):
7
21
  // This implementation is simple but slow
@@ -24,17 +38,18 @@ export function removeUnusedVariables(program: babel.NodePath<t.Program>) {
24
38
  case "hoisted":
25
39
  case "module":
26
40
  if (binding.references === 0 && !binding.path.removed) {
27
- if (isPathValid(binding.path.parentPath, t.isImportDeclaration)) {
28
41
  const parent = binding.path.parentPath;
42
+ if (isPathValid(parent, t.isImportDeclaration)) {
29
43
  if (parent.node.specifiers.length === 1) {
30
44
  parent.remove();
31
45
  } else {
32
46
  binding.path.remove();
33
47
  }
34
- } else {
48
+ dirty = true;
49
+ } else if (!(isInvalidForRemoval(binding.path))) {
35
50
  binding.path.remove();
51
+ dirty = true;
36
52
  }
37
- dirty = true;
38
53
  }
39
54
  break;
40
55
  case "local":
@@ -1,5 +1,5 @@
1
1
  import middleware from "solid-start:middleware";
2
- import { defineHandler, getCookie, H3, type H3Event, redirect, setCookie } from "h3";
2
+ import { defineHandler, getCookie, H3, type H3Event, redirect, setCookie } from "h3/generic";
3
3
  import { join } from "pathe";
4
4
  import type { JSX } from "solid-js";
5
5
  import { sharedConfig } from "solid-js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Creates a debounced function that delays the invocation of a function
3
+ */
4
+ export const debounce = <T extends (...args: any[]) => void>(cb: T, debounceMs: number) => {
5
+ let timeout: NodeJS.Timeout | undefined;
6
+ return (...args: Parameters<T>) => {
7
+ clearTimeout(timeout);
8
+ timeout = setTimeout(() => cb(...args), debounceMs)
9
+ }
10
+ }