veryfront 0.0.83 → 0.0.86

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 (41) hide show
  1. package/README.md +21 -60
  2. package/esm/deno.js +1 -1
  3. package/esm/src/cli/app/components/list-select.js +2 -2
  4. package/esm/src/cli/app/index.d.ts +1 -1
  5. package/esm/src/cli/app/index.d.ts.map +1 -1
  6. package/esm/src/cli/app/index.js +26 -33
  7. package/esm/src/cli/app/state.d.ts +3 -0
  8. package/esm/src/cli/app/state.d.ts.map +1 -1
  9. package/esm/src/cli/app/state.js +4 -0
  10. package/esm/src/cli/app/views/dashboard.d.ts.map +1 -1
  11. package/esm/src/cli/app/views/dashboard.js +46 -58
  12. package/esm/src/cli/app/views/startup.d.ts +39 -0
  13. package/esm/src/cli/app/views/startup.d.ts.map +1 -0
  14. package/esm/src/cli/app/views/startup.js +103 -0
  15. package/esm/src/cli/commands/dev.js +2 -2
  16. package/esm/src/cli/commands/new.js +1 -1
  17. package/esm/src/cli/ui/colors.d.ts +8 -0
  18. package/esm/src/cli/ui/colors.d.ts.map +1 -1
  19. package/esm/src/cli/ui/colors.js +34 -0
  20. package/esm/src/cli/ui/dot-matrix.d.ts +8 -0
  21. package/esm/src/cli/ui/dot-matrix.d.ts.map +1 -1
  22. package/esm/src/cli/ui/dot-matrix.js +67 -2
  23. package/esm/src/cli/ui/tui.js +1 -1
  24. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  25. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +28 -6
  26. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  27. package/esm/src/transforms/esm/http-cache.js +25 -17
  28. package/package.json +1 -1
  29. package/src/deno.js +1 -1
  30. package/src/src/cli/app/components/list-select.ts +2 -2
  31. package/src/src/cli/app/index.ts +34 -35
  32. package/src/src/cli/app/state.ts +7 -0
  33. package/src/src/cli/app/views/dashboard.ts +47 -61
  34. package/src/src/cli/app/views/startup.ts +132 -0
  35. package/src/src/cli/commands/dev.ts +2 -2
  36. package/src/src/cli/commands/new.ts +1 -1
  37. package/src/src/cli/ui/colors.ts +37 -0
  38. package/src/src/cli/ui/dot-matrix.ts +77 -1
  39. package/src/src/cli/ui/tui.ts +1 -1
  40. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +43 -30
  41. package/src/src/transforms/esm/http-cache.ts +26 -17
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { box } from "../../ui/box.js";
8
- import { brand, dim, error, muted, success } from "../../ui/colors.js";
8
+ import { brand, dim, error, muted } from "../../ui/colors.js";
9
9
  import { getTerminalWidth } from "../../ui/layout.js";
10
10
  import { getAgentFaceWithText } from "../../ui/dot-matrix.js";
11
11
  import { renderList } from "../components/list-select.js";
@@ -27,7 +27,7 @@ export function renderDashboard(state: AppState): string {
27
27
 
28
28
  if (hasProjects) {
29
29
  const isActive = state.activeList === "projects";
30
- lines.push(renderSection("Local Projects", state.projects.items.length, isActive));
30
+ lines.push(renderSection("Local", state.projects.items.length, isActive));
31
31
  lines.push(
32
32
  renderList(state.projects, {
33
33
  maxWidth: maxListWidth,
@@ -47,10 +47,10 @@ export function renderDashboard(state: AppState): string {
47
47
  const end = Math.min(start + visibleCount, state.remote.projects.length);
48
48
  const visibleProjects = state.remote.projects.slice(start, end);
49
49
 
50
- lines.push(renderSection("Remote Projects", state.remote.projects.length, isRemoteActive));
50
+ lines.push(renderSection("Remote", state.remote.projects.length, isRemoteActive));
51
51
 
52
52
  if (start > 0) {
53
- lines.push(` ${dim("more above")}`);
53
+ lines.push(` ${dim("↑")} ${dim("more above")}`);
54
54
  }
55
55
 
56
56
  visibleProjects.forEach((p, i) => {
@@ -68,7 +68,7 @@ export function renderDashboard(state: AppState): string {
68
68
  });
69
69
 
70
70
  if (end < state.remote.projects.length) {
71
- lines.push(` ${dim("more below")}`);
71
+ lines.push(` ${dim("↓")} ${dim("more below")}`);
72
72
  }
73
73
  lines.push("");
74
74
  }
@@ -93,27 +93,26 @@ export function renderDashboard(state: AppState): string {
93
93
  }
94
94
 
95
95
  /**
96
- * Render the banner with agent face and server info
96
+ * Render the banner with agent face and server info inside a box
97
97
  */
98
98
  function renderBanner(state: AppState): string {
99
- const serverDot = state.server.running ? success("●") : error("●");
100
- const mcpDot = state.mcp.enabled ? success("●") : dim("○");
101
-
99
+ const termWidth = Math.min(getTerminalWidth() - 4, 80);
102
100
  const textLines: string[] = [];
103
101
 
104
- textLines.push(`${serverDot} ${dim("Server running")}`);
105
- textLines.push(` ${brand(state.server.url)}`);
102
+ textLines.push("");
103
+ textLines.push(`${brand("Veryfront Code")} ${dim("is now running")}`);
104
+ textLines.push("");
106
105
 
107
- if (state.mcp.enabled) {
108
- textLines.push(`${mcpDot} ${dim("MCP")}`);
109
- if (state.mcp.transport === "http") {
110
- const port = state.mcp.httpPort ?? 9999;
111
- textLines.push(` ${brand(`http://veryfront.me:${port}/mcp`)}`);
112
- } else {
113
- textLines.push(` ${dim("stdio")}`);
114
- }
106
+ // Server URL and MCP URL - always reserve both lines to prevent jumps
107
+ textLines.push(`${dim("Url")} ${brand(state.server.url)}`);
108
+ if (state.mcp.enabled && state.mcp.transport === "http") {
109
+ const port = state.mcp.httpPort ?? 9999;
110
+ textLines.push(`${dim("Mcp")} ${brand(`http://veryfront.me:${port}/mcp`)}`);
111
+ } else {
112
+ textLines.push("");
115
113
  }
116
114
 
115
+ // Errors/warnings on separate line if any
117
116
  const { errors, warnings } = state.server;
118
117
  if (errors > 0 || warnings > 0) {
119
118
  const parts: string[] = [];
@@ -122,58 +121,56 @@ function renderBanner(state: AppState): string {
122
121
  textLines.push(parts.join(" "));
123
122
  }
124
123
 
125
- return getAgentFaceWithText(textLines, {
124
+ // Pad to 7 text lines (matching avatar height) for consistent title position
125
+ while (textLines.length < 7) {
126
+ textLines.push("");
127
+ }
128
+
129
+ const content = getAgentFaceWithText(textLines, {
126
130
  litColor: "\x1b[38;2;252;143;93m", // Veryfront brand orange
127
131
  });
132
+
133
+ return box(content, {
134
+ style: "rounded",
135
+ width: termWidth,
136
+ paddingX: 2,
137
+ paddingY: 1,
138
+ borderColor: "\x1b[2m", // Dim to match footer
139
+ });
128
140
  }
129
141
 
130
142
  /**
131
143
  * Render a section header
132
144
  */
133
- function renderSection(title: string, count: number, isActive = true): string {
145
+ function renderSection(title: string, _count: number, isActive = true): string {
134
146
  const indicator = isActive ? brand("›") : " ";
135
147
  const titleText = isActive ? title : dim(title);
136
- return ` ${indicator} ${titleText} ${dim(`(${count})`)}`;
148
+ return ` ${indicator} ${titleText}`;
137
149
  }
138
150
 
139
151
  /**
140
152
  * Render the help bar at the bottom
141
153
  */
142
154
  function renderHelpBar(state: AppState): string {
143
- const parts: string[] = [];
144
-
145
- const hasProjects = state.projects.items.length > 0;
146
- const hasExamples = state.examples.items.length > 0;
147
- const hasRemoteProjects = state.remote.user && state.remote.projects.length > 0;
148
-
149
- // Count sections for tab switching
150
- const sectionCount = [hasProjects, hasExamples, hasRemoteProjects].filter(Boolean).length;
151
-
152
- if (sectionCount > 1) {
153
- parts.push(dim("tab switch"));
155
+ // Minimal by default, ? reveals all
156
+ if (!state.showHelp) {
157
+ const userInfo = state.remote.user ? ` ${dim("-")} ${brand(state.remote.user.email)}` : "";
158
+ return ` ${dim("↑↓ select enter open ? more q quit")}${userInfo}`;
154
159
  }
155
160
 
156
- parts.push(dim("↑↓ nav"));
157
-
158
- if (hasProjects || hasExamples || hasRemoteProjects) {
159
- parts.push(dim("o open"), dim("s studio"), dim("i ide"));
160
- }
161
+ // Expanded help
162
+ const lines: string[] = [];
163
+ lines.push(` ${dim("o")} open ${dim("s")} studio ${dim("i")} ide`);
161
164
 
162
165
  if (!state.remote.user) {
163
- parts.push(dim("a login"));
166
+ lines.push(` ${dim("n")} new ${dim("a")} login`);
164
167
  } else {
165
- // Show context-aware actions based on active list
166
- if (state.activeList === "projects") {
167
- parts.push(dim("p pull"), dim("u push"));
168
- } else if (state.activeList === "remoteProjects") {
169
- parts.push(dim("p pull"));
170
- }
171
- parts.push(dim("n new"), dim("x logout"));
168
+ lines.push(` ${dim("n")} new ${dim("p")} pull ${dim("u")} push ${dim("x")} logout`);
172
169
  }
173
170
 
174
- parts.push(dim("? help"), dim("q quit"));
171
+ lines.push(` ${dim("? hide q quit")}`);
175
172
 
176
- return ` ${parts.join(" ")}`;
173
+ return lines.join("\n");
177
174
  }
178
175
 
179
176
  /**
@@ -185,7 +182,7 @@ export function renderDashboardBoxed(state: AppState): string {
185
182
 
186
183
  return box(content, {
187
184
  style: "rounded",
188
- title: "Veryfront",
185
+ title: "Veryfront Code",
189
186
  titleColor: "\x1b[38;2;252;143;93m",
190
187
  width: termWidth,
191
188
  paddingX: 1,
@@ -197,16 +194,5 @@ export function renderDashboardBoxed(state: AppState): string {
197
194
  * Render empty state when no projects found
198
195
  */
199
196
  export function renderEmptyState(): string {
200
- return [
201
- "",
202
- ` ${dim("No projects found.")}`,
203
- "",
204
- ` ${dim("Get started:")}`,
205
- ` ${brand("[n]")} Create a new project`,
206
- ` ${brand("[t]")} Browse templates`,
207
- "",
208
- ` ${dim("Or run with a project directory:")}`,
209
- ` ${muted("deno task start --project ./my-project")}`,
210
- "",
211
- ].join("\n");
197
+ return `\n ${dim("No projects.")} ${brand("n")} ${dim("to create")}\n`;
212
198
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Startup View
3
+ *
4
+ * Shows loading progress with consistent box sizing.
5
+ * Displays avatar, title, and step checklist.
6
+ */
7
+
8
+ import { box } from "../../ui/box.js";
9
+ import { brand, dim, shimmer } from "../../ui/colors.js";
10
+
11
+ // Dim orange for completed steps - matches the trailing dots in spinning animation
12
+ const dimOrange = (text: string) => `\x1b[38;2;180;100;65m${text}\x1b[0m`;
13
+ import { getTerminalWidth } from "../../ui/layout.js";
14
+ import { getAgentFaceWithText, getSpinningAgentFace } from "../../ui/dot-matrix.js";
15
+
16
+ export interface StartupStep {
17
+ label: string;
18
+ status: "pending" | "active" | "done";
19
+ }
20
+
21
+ export interface StartupState {
22
+ steps: StartupStep[];
23
+ serverUrl?: string;
24
+ mcpUrl?: string;
25
+ ready: boolean;
26
+ /** Animation frame counter for shimmer effect */
27
+ frame: number;
28
+ }
29
+
30
+ /**
31
+ * Render the startup view inside a consistent-sized box
32
+ */
33
+ export function renderStartup(state: StartupState): string {
34
+ const termWidth = Math.min(getTerminalWidth() - 4, 80);
35
+ const textLines: string[] = [];
36
+
37
+ if (state.ready) {
38
+ // Running state - always reserve space for both URL lines to prevent jumps
39
+ textLines.push("");
40
+ textLines.push(`${brand("Veryfront Code")} ${dim("is now running")}`);
41
+ textLines.push("");
42
+ textLines.push(state.serverUrl ? `${dim("Url")} ${brand(state.serverUrl)}` : "");
43
+ textLines.push(state.mcpUrl ? `${dim("Mcp")} ${brand(state.mcpUrl)}` : "");
44
+ } else {
45
+ // Loading state - match ready state layout
46
+ textLines.push("");
47
+ textLines.push(`${brand("Veryfront Code")} ${dim("starting...")}`);
48
+ textLines.push("");
49
+
50
+ for (const step of state.steps) {
51
+ if (step.status === "done") {
52
+ // Completed: dim orange (fades into background, coherent with avatar)
53
+ textLines.push(`${dimOrange("●")} ${dimOrange(step.label)}`);
54
+ } else if (step.status === "active") {
55
+ // Active: bright orange dot with shimmer text
56
+ textLines.push(`${brand("●")} ${shimmer(step.label, state.frame)}`);
57
+ } else {
58
+ // Pending: gray empty circle
59
+ textLines.push(`${dim("○")} ${dim(step.label)}`);
60
+ }
61
+ }
62
+ }
63
+
64
+ // Pad to 7 text lines (matching avatar height) for consistent title position
65
+ while (textLines.length < 7) {
66
+ textLines.push("");
67
+ }
68
+
69
+ // Use spinning avatar during loading, static when ready or all steps done
70
+ const allStepsDone = state.steps.every((s) => s.status === "done");
71
+ const content = state.ready || allStepsDone
72
+ ? getAgentFaceWithText(textLines, {
73
+ litColor: "\x1b[38;2;252;143;93m", // Veryfront brand orange
74
+ })
75
+ : getSpinningAgentFace(textLines, state.frame, {
76
+ litColor: "\x1b[38;2;252;143;93m", // Veryfront brand orange
77
+ });
78
+
79
+ return box(content, {
80
+ style: "rounded",
81
+ width: termWidth,
82
+ paddingX: 2,
83
+ paddingY: 1,
84
+ borderColor: "\x1b[2m", // Dim to match footer
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Create initial startup state with steps
90
+ */
91
+ export function createStartupState(stepLabels: string[]): StartupState {
92
+ return {
93
+ steps: stepLabels.map((label) => ({ label, status: "pending" })),
94
+ ready: false,
95
+ frame: 0,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Increment animation frame for shimmer effect
101
+ */
102
+ export function incrementFrame(state: StartupState): StartupState {
103
+ return { ...state, frame: state.frame + 1 };
104
+ }
105
+
106
+ /**
107
+ * Set a step to active
108
+ */
109
+ export function setStepActive(state: StartupState, index: number): StartupState {
110
+ const steps = state.steps.map((step, i) => ({
111
+ ...step,
112
+ status: i < index ? "done" : i === index ? "active" : "pending",
113
+ })) as StartupStep[];
114
+
115
+ return { ...state, steps };
116
+ }
117
+
118
+ /**
119
+ * Mark all steps done and set ready
120
+ */
121
+ export function setStartupReady(
122
+ state: StartupState,
123
+ serverUrl: string,
124
+ mcpUrl?: string,
125
+ ): StartupState {
126
+ const steps = state.steps.map((step) => ({
127
+ ...step,
128
+ status: "done" as const,
129
+ }));
130
+
131
+ return { ...state, steps, serverUrl, mcpUrl, ready: true };
132
+ }
@@ -195,8 +195,8 @@ export function devCommand(options: DevOptions): Promise<DevCommandResult> {
195
195
  console.log();
196
196
  console.log(
197
197
  banner({
198
- title: "Veryfront",
199
- subtitle: "is now running",
198
+ title: "Veryfront Code",
199
+ subtitle: "is running",
200
200
  info: {
201
201
  url: serverUrl,
202
202
  ...(projectSlug ? { project: projectSlug } : {}),
@@ -154,7 +154,7 @@ export async function newCommand(
154
154
  return;
155
155
  }
156
156
 
157
- const tui = createTui({ title: "Veryfront", showLogs: true });
157
+ const tui = createTui({ title: "Veryfront Code", showLogs: true });
158
158
  const restore = interceptConsole(tui);
159
159
 
160
160
  const localUrl = `http://${name}.veryfront.me:${port}`;
@@ -157,3 +157,40 @@ export function animatedMatrix(frame: number): string {
157
157
  const state = MATRIX_STATES[frame % MATRIX_STATES.length] ?? ["●", "○", "○"];
158
158
  return state.map((dot) => (dot === "●" ? brand(dot) : muted(dot))).join("");
159
159
  }
160
+
161
+ /**
162
+ * Apply shimmer effect to text - creates a wave of brightness moving across
163
+ * @param text The text to shimmer
164
+ * @param frame Current animation frame (increments over time)
165
+ * @param waveWidth Width of the bright wave (default: 3 characters)
166
+ * @returns Text with shimmer effect applied
167
+ */
168
+ export function shimmer(text: string, frame: number, waveWidth = 3): string {
169
+ // Brand orange gradient: bright → normal → dim
170
+ const bright = (char: string) => applyColor(char, 255, 180, 140, false); // Brighter orange
171
+ const normal = (char: string) => applyColor(char, 252, 143, 93, false); // Brand orange
172
+ const dimmed = (char: string) => applyColor(char, 180, 100, 65, false); // Dimmer orange
173
+
174
+ const len = text.length;
175
+ const wavePos = frame % (len + waveWidth * 2);
176
+
177
+ let result = "";
178
+ for (let i = 0; i < len; i++) {
179
+ const char = text[i]!;
180
+ const distFromWave = i - (wavePos - waveWidth);
181
+
182
+ if (distFromWave >= 0 && distFromWave < waveWidth) {
183
+ // In the bright wave
184
+ const intensity = 1 - Math.abs(distFromWave - waveWidth / 2) / (waveWidth / 2);
185
+ if (intensity > 0.6) {
186
+ result += bright(char);
187
+ } else {
188
+ result += normal(char);
189
+ }
190
+ } else {
191
+ result += dimmed(char);
192
+ }
193
+ }
194
+
195
+ return result;
196
+ }
@@ -76,6 +76,14 @@ const COMPACT_OPTIONS: Partial<DotMatrixOptions> = {
76
76
 
77
77
  const RESET = "\x1b[0m";
78
78
 
79
+ // Colors for spinning animation - shades of orange
80
+ const SPIN_COLORS = {
81
+ bright: "\x1b[38;2;255;165;120m", // Bright orange (leading edge, toned down)
82
+ orange: "\x1b[38;2;252;143;93m", // Brand orange
83
+ mid: "\x1b[38;2;200;110;70m", // Mid orange (trailing)
84
+ dim: "\x1b[38;2;140;80;50m", // Dim orange/brown
85
+ };
86
+
79
87
  function resolveOptions(options: DotMatrixOptions): Required<DotMatrixOptions> {
80
88
  if (options.compact) return { ...DEFAULT_OPTIONS, ...COMPACT_OPTIONS, ...options };
81
89
  return { ...DEFAULT_OPTIONS, ...options };
@@ -92,12 +100,65 @@ function renderPattern(pattern: number[][], opts: Required<DotMatrixOptions>): s
92
100
  });
93
101
  }
94
102
 
103
+ /**
104
+ * Render the pattern with a spinning blade effect
105
+ * The blade sweeps across the lit dots, creating orange → purple gradient
106
+ */
107
+ function renderSpinningPattern(
108
+ pattern: number[][],
109
+ frame: number,
110
+ opts: Required<DotMatrixOptions>,
111
+ ): string[] {
112
+ const centerRow = 3;
113
+ const centerCol = 3;
114
+ const totalFrames = 16; // Full rotation in 16 frames
115
+ const bladeAngle = ((frame % totalFrames) / totalFrames) * Math.PI * 2;
116
+
117
+ return pattern.map((row, rowIdx) => {
118
+ const dots = row.map((dot, colIdx) => {
119
+ if (dot !== 1) {
120
+ return `${opts.offColor}${opts.offChar}${RESET}`;
121
+ }
122
+
123
+ // Calculate angle from center to this dot
124
+ const dy = rowIdx - centerRow;
125
+ const dx = colIdx - centerCol;
126
+ let dotAngle = Math.atan2(dy, dx);
127
+ if (dotAngle < 0) dotAngle += Math.PI * 2;
128
+
129
+ // Calculate angular distance from the blade
130
+ let angleDiff = dotAngle - bladeAngle;
131
+ if (angleDiff < 0) angleDiff += Math.PI * 2;
132
+ if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff;
133
+
134
+ // Color based on angular distance from blade
135
+ const normalizedDiff = angleDiff / Math.PI; // 0 = at blade, 1 = opposite
136
+ let color: string;
137
+ if (normalizedDiff < 0.15) {
138
+ color = SPIN_COLORS.bright; // Leading edge - brightest orange
139
+ } else if (normalizedDiff < 0.35) {
140
+ color = SPIN_COLORS.orange; // Near blade - brand orange
141
+ } else if (normalizedDiff < 0.6) {
142
+ color = SPIN_COLORS.mid; // Trailing - mid orange
143
+ } else {
144
+ color = SPIN_COLORS.dim; // Far from blade - dim orange
145
+ }
146
+
147
+ return `${color}${opts.litChar}${RESET}`;
148
+ });
149
+ return opts.prefix + dots.join(opts.spacing);
150
+ });
151
+ }
152
+
95
153
  function renderPatternWithText(
96
154
  pattern: number[][],
97
155
  textLines: string[],
98
156
  opts: Required<DotMatrixOptions>,
157
+ spinFrame?: number,
99
158
  ): string {
100
- const faceLines = renderPattern(pattern, opts);
159
+ const faceLines = spinFrame !== undefined
160
+ ? renderSpinningPattern(pattern, spinFrame, opts)
161
+ : renderPattern(pattern, opts);
101
162
 
102
163
  const faceHeight = faceLines.length;
103
164
  const startLine = Math.floor((faceHeight - textLines.length) / 2);
@@ -164,6 +225,21 @@ export function getAgentFaceWithText(
164
225
  return renderPatternWithText(AGENT_FACE, textLines, resolveOptions(options));
165
226
  }
166
227
 
228
+ /**
229
+ * Get the agent face with spinning blade animation
230
+ * Orange and purple dots rotate around the logo
231
+ * @param textLines Text to show next to the face
232
+ * @param frame Animation frame (0-15 for full rotation)
233
+ * @param options Dot matrix options
234
+ */
235
+ export function getSpinningAgentFace(
236
+ textLines: string[],
237
+ frame: number,
238
+ options: DotMatrixOptions = {},
239
+ ): string {
240
+ return renderPatternWithText(AGENT_FACE, textLines, resolveOptions(options), frame);
241
+ }
242
+
167
243
  /**
168
244
  * Animated dot matrix display with spinner support
169
245
  */
@@ -149,7 +149,7 @@ export function createTui(cfg: TuiConfig = {}): {
149
149
  cleanup: () => void;
150
150
  render: () => void;
151
151
  } {
152
- config = { title: "Veryfront", showLogs: true, ...cfg };
152
+ config = { title: "Veryfront Code", showLogs: true, ...cfg };
153
153
  state = {
154
154
  status: "Initializing...",
155
155
  statusType: "loading",
@@ -143,24 +143,37 @@ export class SSRModuleLoader {
143
143
  const bundleMatch = errorMsg.match(/veryfront-http-bundle\/http-([a-f0-9]+)\.mjs/);
144
144
  if (bundleMatch) {
145
145
  const hash = bundleMatch[1]!;
146
- logger.warn(
147
- "[SSR-MODULE-LOADER] Import failed due to missing HTTP bundle, attempting recovery",
148
- {
149
- file: filePath.slice(-40),
150
- hash,
151
- },
152
- );
146
+ const cacheDir = getHttpBundleCacheDir();
147
+
148
+ logger.error("[SSR-MODULE-LOADER] Missing HTTP bundle after ensureHttpBundlesExist", {
149
+ file: filePath.slice(-40),
150
+ hash,
151
+ tempPath: cacheEntry.tempPath,
152
+ contentHash: cacheEntry.contentHash,
153
+ cacheDir,
154
+ expectedPath: `${cacheDir}/http-${hash}.mjs`,
155
+ });
156
+
153
157
  const { recoverHttpBundleByHash } = await import(
154
158
  "../../../transforms/esm/http-cache.js"
155
159
  );
156
- const cacheDir = getHttpBundleCacheDir();
157
160
  const recovered = await recoverHttpBundleByHash(hash, cacheDir);
158
161
  if (recovered) {
159
- logger.info("[SSR-MODULE-LOADER] HTTP bundle recovered, retrying import", { hash });
162
+ logger.info("[SSR-MODULE-LOADER] HTTP bundle recovered, retrying import", {
163
+ hash,
164
+ file: filePath.slice(-40),
165
+ });
160
166
  mod = await import(
161
167
  `file://${cacheEntry.tempPath}?v=${cacheEntry.contentHash}&retry=1`
162
168
  ) as Record<string, unknown>;
163
169
  } else {
170
+ logger.error("[SSR-MODULE-LOADER] HTTP bundle recovery failed", {
171
+ hash,
172
+ file: filePath.slice(-40),
173
+ cacheDir,
174
+ hint:
175
+ "Bundle may have expired from Redis (24h TTL) while transform was still cached",
176
+ });
164
177
  throw importError;
165
178
  }
166
179
  } else {
@@ -444,13 +457,13 @@ export class SSRModuleLoader {
444
457
  const cacheDir = getHttpBundleCacheDir();
445
458
  const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
446
459
  if (failed.length > 0) {
447
- logger.warn(
448
- "[SSR-MODULE-LOADER] In-memory cached module has unrecoverable HTTP bundles, re-transforming",
449
- {
450
- file: filePath.slice(-40),
451
- failed,
452
- },
453
- );
460
+ logger.warn("[SSR-MODULE-LOADER] Unrecoverable HTTP bundles, re-transforming", {
461
+ file: filePath.slice(-40),
462
+ failed,
463
+ totalBundles: bundlePaths.length,
464
+ cacheDir,
465
+ source: "memory-cache",
466
+ });
454
467
  globalModuleCache.delete(contentCacheKey);
455
468
  globalModuleCache.delete(filePathCacheKey);
456
469
  // Fall through to Redis or fresh transform
@@ -487,13 +500,13 @@ export class SSRModuleLoader {
487
500
  const cacheDir = getHttpBundleCacheDir();
488
501
  const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
489
502
  if (failed.length > 0) {
490
- logger.warn(
491
- "[SSR-MODULE-LOADER] Redis cached code has unrecoverable HTTP bundles, re-transforming",
492
- {
493
- file: filePath.slice(-40),
494
- failed,
495
- },
496
- );
503
+ logger.warn("[SSR-MODULE-LOADER] Unrecoverable HTTP bundles, re-transforming", {
504
+ file: filePath.slice(-40),
505
+ failed,
506
+ totalBundles: bundlePaths.length,
507
+ cacheDir,
508
+ source: "redis-cache",
509
+ });
497
510
  httpBundlesOk = false;
498
511
  }
499
512
  }
@@ -642,13 +655,13 @@ export class SSRModuleLoader {
642
655
  const cacheDir = getHttpBundleCacheDir();
643
656
  const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
644
657
  if (failed.length > 0) {
645
- logger.warn(
646
- "[SSR-MODULE-LOADER] Some HTTP bundles could not be recovered",
647
- {
648
- file: filePath.slice(-40),
649
- failed,
650
- },
651
- );
658
+ logger.warn("[SSR-MODULE-LOADER] Unrecoverable HTTP bundles", {
659
+ file: filePath.slice(-40),
660
+ failed,
661
+ totalBundles: bundlePaths.length,
662
+ cacheDir,
663
+ source: "fresh-transform",
664
+ });
652
665
  }
653
666
  }
654
667