vorma 0.83.0 → 0.85.0-pre.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.
@@ -1,5 +1,6 @@
1
1
  import { getAnchorDetailsFromEvent, getHrefDetails } from "vorma/kit/url";
2
2
  import { navigationStateManager, vormaNavigate } from "./client.ts";
3
+ import { effectuateRedirectDataResult } from "./redirects/redirects.ts";
3
4
  import { saveScrollState } from "./scroll_state_manager.ts";
4
5
 
5
6
  type LinkOnClickCallback<E extends Event> = (event: E) => void | Promise<void>;
@@ -173,29 +174,62 @@ export function __makeLinkOnClickFn<E extends Event>(
173
174
 
174
175
  if (!control.promise) return;
175
176
 
176
- const res = await control.promise;
177
-
178
- if (!res) {
179
- // If not here, loading indicator can get stuck on
180
- // following redirects
181
- const targetUrl = new URL(anchor.href, window.location.href)
182
- .href;
183
- navigationStateManager.removeNavigation(targetUrl);
184
- return;
185
- }
186
-
187
- await callbacks.beforeRender?.(e);
188
-
177
+ const outcome = await control.promise;
189
178
  const targetUrl = new URL(anchor.href, window.location.href).href;
190
- const entry = navigationStateManager.getNavigation(targetUrl);
191
- if (entry) {
192
- await navigationStateManager["processNavigationResult"](
193
- res,
194
- entry,
195
- );
196
- }
197
179
 
198
- await callbacks.afterRender?.(e);
180
+ // Handle outcome based on type (discriminated union)
181
+ switch (outcome.type) {
182
+ case "aborted":
183
+ // Navigation was aborted - clean up to prevent stuck loading indicator
184
+ navigationStateManager.removeNavigation(targetUrl);
185
+ return;
186
+
187
+ case "redirect": {
188
+ // Call beforeRender while entry still exists (consistent with success case)
189
+ await callbacks.beforeRender?.(e);
190
+
191
+ // Clean up before redirect to prevent race conditions
192
+ navigationStateManager.removeNavigation(targetUrl);
193
+
194
+ // Effectuate the redirect
195
+ await effectuateRedirectDataResult(
196
+ outcome.redirectData,
197
+ outcome.props.redirectCount || 0,
198
+ outcome.props,
199
+ );
200
+
201
+ // Call afterRender after redirect effectuation
202
+ await callbacks.afterRender?.(e);
203
+ return;
204
+ }
205
+
206
+ case "success": {
207
+ // Call beforeRender before processing (matches original behavior)
208
+ await callbacks.beforeRender?.(e);
209
+
210
+ // Process the successful navigation if entry still exists
211
+ const entry =
212
+ navigationStateManager.getNavigation(targetUrl);
213
+ if (entry) {
214
+ await navigationStateManager.processSuccessfulNavigation(
215
+ outcome,
216
+ entry,
217
+ );
218
+ }
219
+
220
+ // Call afterRender after processing (matches original behavior)
221
+ await callbacks.afterRender?.(e);
222
+ return;
223
+ }
224
+
225
+ default: {
226
+ // Exhaustiveness check
227
+ const _exhaustive: never = outcome;
228
+ throw new Error(
229
+ `Unexpected outcome type: ${(_exhaustive as any).type}`,
230
+ );
231
+ }
232
+ }
199
233
  }
200
234
  };
201
235
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vorma",
3
- "version": "0.83.0",
3
+ "version": "0.85.0-pre.0",
4
4
  "description": "CLI for creating new Vorma applications.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",
@@ -24,8 +24,8 @@
24
24
  "@clack/prompts": "^0.11.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@types/node": "^24.2.1",
28
- "typescript": "^5.9.2"
27
+ "@types/node": "^24.10.7",
28
+ "typescript": "^5.9.3"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=22.11.0"
@@ -13,11 +13,11 @@ importers:
13
13
  version: 0.11.0
14
14
  devDependencies:
15
15
  '@types/node':
16
- specifier: ^24.2.1
17
- version: 24.2.1
16
+ specifier: ^24.10.7
17
+ version: 24.10.7
18
18
  typescript:
19
- specifier: ^5.9.2
20
- version: 5.9.2
19
+ specifier: ^5.9.3
20
+ version: 5.9.3
21
21
 
22
22
  packages:
23
23
 
@@ -27,8 +27,8 @@ packages:
27
27
  '@clack/prompts@0.11.0':
28
28
  resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
29
29
 
30
- '@types/node@24.2.1':
31
- resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==}
30
+ '@types/node@24.10.7':
31
+ resolution: {integrity: sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ==}
32
32
 
33
33
  picocolors@1.1.1:
34
34
  resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -36,13 +36,13 @@ packages:
36
36
  sisteransi@1.0.5:
37
37
  resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
38
38
 
39
- typescript@5.9.2:
40
- resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
39
+ typescript@5.9.3:
40
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
41
41
  engines: {node: '>=14.17'}
42
42
  hasBin: true
43
43
 
44
- undici-types@7.10.0:
45
- resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
44
+ undici-types@7.16.0:
45
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
46
46
 
47
47
  snapshots:
48
48
 
@@ -57,14 +57,14 @@ snapshots:
57
57
  picocolors: 1.1.1
58
58
  sisteransi: 1.0.5
59
59
 
60
- '@types/node@24.2.1':
60
+ '@types/node@24.10.7':
61
61
  dependencies:
62
- undici-types: 7.10.0
62
+ undici-types: 7.16.0
63
63
 
64
64
  picocolors@1.1.1: {}
65
65
 
66
66
  sisteransi@1.0.5: {}
67
67
 
68
- typescript@5.9.2: {}
68
+ typescript@5.9.3: {}
69
69
 
70
- undici-types@7.10.0: {}
70
+ undici-types@7.16.0: {}
@@ -1,23 +1,74 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
1
4
  type VormaVitePluginConfig = {
2
5
  rollupInput: ReadonlyArray<string>;
3
6
  publicPathPrefix: string;
4
7
  staticPublicAssetMap: Record<string, string>;
5
8
  buildtimePublicURLFuncName: string;
9
+ filemapJSONPath: string;
6
10
  ignoredPatterns: ReadonlyArray<string>;
7
11
  dedupeList: ReadonlyArray<string>;
8
12
  };
9
13
 
10
14
  export default function vormaVitePlugin(config: VormaVitePluginConfig): any {
15
+ // Cache for dev mode filemap reading.
16
+ // In dev mode, we read from the JSON file so we can pick up changes
17
+ // without restarting Vite. The mtime check allows us to avoid re-reading
18
+ // the file on every transform if it hasn't changed.
19
+ let cachedMap: Record<string, string> | null = null;
20
+ let cachedMtime: number = 0;
21
+ let isDev = false;
22
+ let resolvedFilemapPath: string | null = null;
23
+
24
+ /**
25
+ * Gets the current filemap, reading from disk in dev mode.
26
+ * In production builds, uses the static map passed at plugin creation.
27
+ */
28
+ function getFilemap(): Record<string, string> {
29
+ if (!isDev) {
30
+ return config.staticPublicAssetMap;
31
+ }
32
+
33
+ if (!resolvedFilemapPath) {
34
+ resolvedFilemapPath = resolve(
35
+ process.cwd(),
36
+ config.filemapJSONPath,
37
+ );
38
+ }
39
+
40
+ try {
41
+ const stat = statSync(resolvedFilemapPath);
42
+ const mtime = stat.mtimeMs;
43
+
44
+ // Return cached version if file hasn't changed
45
+ if (cachedMap && mtime === cachedMtime) {
46
+ return cachedMap;
47
+ }
48
+
49
+ const content = readFileSync(resolvedFilemapPath, "utf-8");
50
+ cachedMap = JSON.parse(content);
51
+ cachedMtime = mtime;
52
+ return cachedMap!;
53
+ } catch {
54
+ // Fallback to initial config if file can't be read.
55
+ // This handles the case where the JSON file doesn't exist yet
56
+ // (e.g., on first build before Wave has written it).
57
+ return config.staticPublicAssetMap;
58
+ }
59
+ }
60
+
11
61
  return {
12
62
  name: "vorma-vite-plugin",
63
+
13
64
  config(c: any, { command }: any) {
65
+ isDev = command === "serve";
66
+
14
67
  const mp = c.build?.modulePreload;
15
68
  const roi = c.build?.rollupOptions?.input;
16
69
  const ign = c.server?.watch?.ignored;
17
70
  const dedupe = c.resolve?.dedupe;
18
71
 
19
- const isDev = command === "serve";
20
-
21
72
  return {
22
73
  base: isDev ? "/" : config.publicPathPrefix,
23
74
  build: {
@@ -65,6 +116,48 @@ export default function vormaVitePlugin(config: VormaVitePluginConfig): any {
65
116
  },
66
117
  };
67
118
  },
119
+
120
+ /**
121
+ * Configures the dev server with an endpoint for filemap cache invalidation.
122
+ * Wave calls this endpoint after updating public static files, which:
123
+ * 1. Clears the cached filemap so the next transform reads fresh data
124
+ * 2. Invalidates all modules in Vite's module graph
125
+ * 3. Triggers a browser reload via Vite's HMR websocket
126
+ *
127
+ * This is much faster than cycling Vite (stopping and restarting the process).
128
+ */
129
+ configureServer(server: any) {
130
+ server.middlewares.use((req: any, res: any, next: any) => {
131
+ if (req.url !== "/__vorma_invalidate_filemap") {
132
+ return next();
133
+ }
134
+
135
+ console.log(
136
+ "[vorma-vite-plugin] Filemap invalidation triggered",
137
+ );
138
+
139
+ // Clear the filemap cache so the next transform reads fresh data
140
+ cachedMap = null;
141
+ cachedMtime = 0;
142
+
143
+ // Invalidate all modules in Vite's module graph.
144
+ // This is simpler than tracking which specific modules use
145
+ // waveBuildtimeURL() and fast enough for typical project sizes
146
+ // (a few ms for hundreds of modules).
147
+ for (const mod of server.moduleGraph.idToModuleMap.values()) {
148
+ server.moduleGraph.invalidateModule(mod);
149
+ }
150
+
151
+ // Trigger a full browser reload via Vite's HMR websocket.
152
+ // The browser will re-request modules, Vite will re-transform them
153
+ // (cache miss due to invalidation), and they'll get the new URLs.
154
+ server.ws.send({ type: "full-reload" });
155
+
156
+ res.statusCode = 200;
157
+ res.end("ok");
158
+ });
159
+ },
160
+
68
161
  transform(code: any, id: any) {
69
162
  const isNodeModules = /node_modules/.test(id);
70
163
  if (isNodeModules) return null;
@@ -77,10 +170,13 @@ export default function vormaVitePlugin(config: VormaVitePluginConfig): any {
77
170
  const needsReplacement = regex.test(code);
78
171
  if (!needsReplacement) return null;
79
172
 
173
+ // Get the current filemap (reads from disk in dev mode)
174
+ const filemap = getFilemap();
175
+
80
176
  const replacedCode = code.replace(
81
177
  regex,
82
178
  (_: any, __: any, assetPath: any) => {
83
- const hashed = config.staticPublicAssetMap[assetPath];
179
+ const hashed = filemap[assetPath];
84
180
  if (!hashed) return `"${assetPath}"`;
85
181
  return `"${config.publicPathPrefix}${hashed}"`;
86
182
  },