@caryhu/codemine-forge 0.0.1-alpha.0 → 0.0.1-alpha.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 (50) hide show
  1. package/README.md +8 -4
  2. package/dist/build-pipeline/browser-bundle.d.ts +2 -1
  3. package/dist/build-pipeline/browser-bundle.js +384 -24
  4. package/dist/build-pipeline/diagnostics.d.ts +1 -0
  5. package/dist/build-pipeline/diagnostics.js +11 -2
  6. package/dist/build-pipeline/index.d.ts +2 -2
  7. package/dist/build-pipeline/index.js +2 -1
  8. package/dist/build-pipeline/pipeline.d.ts +2 -0
  9. package/dist/build-pipeline/pipeline.js +149 -28
  10. package/dist/build-pipeline/types.d.ts +24 -2
  11. package/dist/build-pipeline/vite-config.d.ts +8 -0
  12. package/dist/build-pipeline/vite-config.js +215 -0
  13. package/dist/dev-server-preview/index.d.ts +2 -2
  14. package/dist/dev-server-preview/index.js +2 -1
  15. package/dist/dev-server-preview/preview-document.d.ts +2 -1
  16. package/dist/dev-server-preview/preview-document.js +203 -1
  17. package/dist/dev-server-preview/server.d.ts +8 -1
  18. package/dist/dev-server-preview/server.js +248 -33
  19. package/dist/dev-server-preview/types.d.ts +40 -1
  20. package/dist/forge-controller.js +1 -0
  21. package/dist/index.d.ts +5 -4
  22. package/dist/index.js +16 -2
  23. package/dist/package-resolution/diagnostics.d.ts +1 -0
  24. package/dist/package-resolution/diagnostics.js +10 -1
  25. package/dist/package-resolution/errors.d.ts +2 -1
  26. package/dist/package-resolution/errors.js +2 -1
  27. package/dist/package-resolution/index.d.ts +2 -2
  28. package/dist/package-resolution/index.js +2 -1
  29. package/dist/package-resolution/resolver.d.ts +3 -0
  30. package/dist/package-resolution/resolver.js +123 -29
  31. package/dist/package-resolution/types.d.ts +7 -1
  32. package/dist/telemetry/index.d.ts +68 -0
  33. package/dist/telemetry/index.js +65 -0
  34. package/dist/terminal-commands/npm-build.js +3 -1
  35. package/dist/terminal-commands/npm-dev.js +3 -1
  36. package/dist/terminal-commands/npm-install.js +7 -2
  37. package/dist/terminal-commands/terminal.d.ts +1 -0
  38. package/dist/terminal-commands/terminal.js +1 -0
  39. package/dist/terminal-commands/types.d.ts +2 -0
  40. package/dist/virtual-file-system/diagnostics.d.ts +1 -0
  41. package/dist/virtual-file-system/diagnostics.js +9 -1
  42. package/dist/virtual-file-system/index.d.ts +4 -2
  43. package/dist/virtual-file-system/index.js +7 -1
  44. package/dist/virtual-file-system/snapshot-serialization.d.ts +3 -0
  45. package/dist/virtual-file-system/snapshot-serialization.js +242 -0
  46. package/dist/virtual-file-system/storage.d.ts +1 -1
  47. package/dist/virtual-file-system/storage.js +41 -18
  48. package/dist/virtual-file-system/types.d.ts +28 -1
  49. package/dist/virtual-file-system/types.js +2 -0
  50. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createForgePreviewDocument = void 0;
3
+ exports.createForgePreviewErrorOverlayDocument = exports.createForgePreviewDocument = void 0;
4
4
  const DEFAULT_HTML_SHELL = [
5
5
  "<!doctype html>",
6
6
  "<html>",
@@ -15,6 +15,9 @@ const DEFAULT_HTML_SHELL = [
15
15
  "</html>",
16
16
  ].join("\n");
17
17
  const PREVIEW_MODULE_SPECIFIER_PREFIX = "__forge_preview__/";
18
+ const PREVIEW_ERROR_OVERLAY_START = "<!-- forge-preview-error-overlay:start -->";
19
+ const PREVIEW_ERROR_OVERLAY_END = "<!-- forge-preview-error-overlay:end -->";
20
+ const PREVIEW_RUNTIME_ERROR_CLIENT_ID = "forge-preview-runtime-error-client";
18
21
  function createForgePreviewDocument(options) {
19
22
  const htmlShell = options.htmlShell ?? DEFAULT_HTML_SHELL;
20
23
  const browserBundle = options.buildResult.browserBundle;
@@ -26,6 +29,7 @@ function createForgePreviewDocument(options) {
26
29
  : previewModuleSpecifiers.get(browserBundle.entryOutputPath) ??
27
30
  browserBundle.entryOutputPath;
28
31
  const metadata = {
32
+ adapter: options.adapter,
29
33
  browserBundle: browserBundle === undefined
30
34
  ? undefined
31
35
  : {
@@ -58,6 +62,7 @@ function createForgePreviewDocument(options) {
58
62
  previewImportMap === undefined
59
63
  ? ""
60
64
  : `<script type="importmap" id="forge-browser-bundle-importmap">${escapeJsonForScript(JSON.stringify(previewImportMap))}</script>`,
65
+ browserBundle === undefined ? "" : createPreviewRuntimeErrorClientScript(),
61
66
  browserBundle === undefined
62
67
  ? ""
63
68
  : `<script type="module" id="forge-browser-bundle-entry">import ${JSON.stringify(previewEntrySpecifier)};</script>`,
@@ -72,6 +77,16 @@ function createForgePreviewDocument(options) {
72
77
  };
73
78
  }
74
79
  exports.createForgePreviewDocument = createForgePreviewDocument;
80
+ function createForgePreviewErrorOverlayDocument(previewDocument, diagnostics) {
81
+ if (diagnostics.length === 0) {
82
+ return previewDocument;
83
+ }
84
+ return {
85
+ ...previewDocument,
86
+ html: injectBeforeBodyEnd(removePreviewErrorOverlay(previewDocument.html), createPreviewErrorOverlay(diagnostics)),
87
+ };
88
+ }
89
+ exports.createForgePreviewErrorOverlayDocument = createForgePreviewErrorOverlayDocument;
75
90
  function createPreviewImportMap(outputFiles, previewModuleSpecifiers) {
76
91
  const importMapFile = outputFiles.find((file) => file.contentType === "application/importmap+json");
77
92
  if (importMapFile === undefined) {
@@ -130,12 +145,199 @@ function injectBeforeBodyEnd(html, injection) {
130
145
  }
131
146
  return `${html}\n${injection}`;
132
147
  }
148
+ function createPreviewRuntimeErrorClientScript() {
149
+ return [
150
+ `<script id="${PREVIEW_RUNTIME_ERROR_CLIENT_ID}">`,
151
+ "(function(){",
152
+ ' var overlaySelector = "[data-forge-preview-runtime-error-overlay]";',
153
+ ' var styleSelector = "style[data-forge-preview-runtime-error-overlay-style]";',
154
+ " function stringify(value){",
155
+ ' if (value instanceof Error) return value.message || value.name || "Runtime error";',
156
+ ' if (typeof value === "string") return value;',
157
+ " if (value === undefined) return \"undefined\";",
158
+ " if (value === null) return \"null\";",
159
+ " try {",
160
+ " var json = JSON.stringify(value);",
161
+ " if (typeof json === \"string\") return json;",
162
+ " } catch (error) {}",
163
+ " return String(value);",
164
+ " }",
165
+ " function stackFor(value){",
166
+ " return value instanceof Error && typeof value.stack === \"string\" && value.stack.length > 0",
167
+ " ? value.stack",
168
+ " : undefined;",
169
+ " }",
170
+ " function formatLocation(event){",
171
+ " var parts = [];",
172
+ " if (typeof event.filename === \"string\" && event.filename.length > 0) parts.push(event.filename);",
173
+ " if (typeof event.lineno === \"number\" && event.lineno > 0) parts.push(\"line \" + event.lineno);",
174
+ " if (typeof event.colno === \"number\" && event.colno > 0) parts.push(\"column \" + event.colno);",
175
+ " return parts.length > 0 ? parts.join(\" · \") : undefined;",
176
+ " }",
177
+ " function createTextElement(tagName, attributeName, text){",
178
+ " var element = document.createElement(tagName);",
179
+ " element.setAttribute(attributeName, \"\");",
180
+ " element.textContent = text;",
181
+ " return element;",
182
+ " }",
183
+ " function ensureStyle(){",
184
+ " if (document.querySelector(styleSelector)) return;",
185
+ " var style = document.createElement(\"style\");",
186
+ " style.setAttribute(\"data-forge-preview-runtime-error-overlay-style\", \"\");",
187
+ " style.textContent = [",
188
+ ' "[data-forge-preview-runtime-error-overlay]{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:flex-start;justify-content:center;padding:32px 16px;background:rgba(15,23,42,.72);color:#f8fafc;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\'Segoe UI\',sans-serif;box-sizing:border-box;}",',
189
+ ' "[data-forge-preview-runtime-error-overlay] *{box-sizing:border-box;}",',
190
+ ' "[data-forge-preview-runtime-error-overlay-panel]{width:min(760px,100%);max-height:calc(100vh - 64px);overflow:auto;border:1px solid rgba(248,113,113,.45);border-radius:8px;background:#111827;box-shadow:0 24px 80px rgba(0,0,0,.42);padding:20px;}",',
191
+ ' "[data-forge-preview-runtime-error-overlay-kicker]{margin:0 0 6px;color:#fca5a5;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;}",',
192
+ ' "[data-forge-preview-runtime-error-overlay-title]{margin:0 0 16px;font-size:20px;line-height:1.25;font-weight:700;}",',
193
+ ' "[data-forge-preview-runtime-error-overlay-message]{margin:0 0 12px;color:#f8fafc;font-size:14px;line-height:1.5;}",',
194
+ ' "[data-forge-preview-runtime-error-overlay-detail]{margin:0;color:#cbd5e1;white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,Consolas,\'Liberation Mono\',monospace;font-size:12px;line-height:1.45;}",',
195
+ " ].join(\"\");",
196
+ " (document.head || document.documentElement).appendChild(style);",
197
+ " }",
198
+ " function showOverlay(diagnostic){",
199
+ " ensureStyle();",
200
+ " var existing = document.querySelector(overlaySelector);",
201
+ " if (existing && existing.parentNode) existing.parentNode.removeChild(existing);",
202
+ " var overlay = document.createElement(\"div\");",
203
+ " overlay.setAttribute(\"data-forge-preview-runtime-error-overlay\", \"\");",
204
+ " overlay.setAttribute(\"role\", \"alert\");",
205
+ " overlay.setAttribute(\"aria-live\", \"assertive\");",
206
+ " var panel = document.createElement(\"section\");",
207
+ " panel.setAttribute(\"data-forge-preview-runtime-error-overlay-panel\", \"\");",
208
+ " panel.appendChild(createTextElement(\"p\", \"data-forge-preview-runtime-error-overlay-kicker\", \"Forge preview\"));",
209
+ " panel.appendChild(createTextElement(\"h1\", \"data-forge-preview-runtime-error-overlay-title\", diagnostic.title));",
210
+ " panel.appendChild(createTextElement(\"p\", \"data-forge-preview-runtime-error-overlay-message\", diagnostic.message));",
211
+ " if (diagnostic.detail) {",
212
+ " panel.appendChild(createTextElement(\"pre\", \"data-forge-preview-runtime-error-overlay-detail\", diagnostic.detail));",
213
+ " }",
214
+ " overlay.appendChild(panel);",
215
+ " (document.body || document.documentElement).appendChild(overlay);",
216
+ " }",
217
+ " window.addEventListener(\"error\", function(event){",
218
+ " showOverlay({",
219
+ " title: \"Preview runtime error\",",
220
+ " message: event.error === undefined || event.error === null",
221
+ " ? event.message || \"Runtime error\"",
222
+ " : stringify(event.error),",
223
+ " detail: stackFor(event.error) || formatLocation(event),",
224
+ " });",
225
+ " event.preventDefault();",
226
+ " });",
227
+ " window.addEventListener(\"unhandledrejection\", function(event){",
228
+ " showOverlay({",
229
+ " title: \"Unhandled promise rejection\",",
230
+ " message: stringify(event.reason),",
231
+ " detail: stackFor(event.reason),",
232
+ " });",
233
+ " event.preventDefault();",
234
+ " });",
235
+ "})();",
236
+ "</script>",
237
+ ].join("\n");
238
+ }
239
+ function createPreviewErrorOverlay(diagnostics) {
240
+ const items = diagnostics.map(createPreviewErrorOverlayItem).join("\n");
241
+ return [
242
+ PREVIEW_ERROR_OVERLAY_START,
243
+ '<style data-forge-preview-error-overlay>',
244
+ "[data-forge-preview-error-overlay]{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:flex-start;justify-content:center;padding:32px 16px;background:rgba(15,23,42,.72);color:#f8fafc;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;box-sizing:border-box;}",
245
+ "[data-forge-preview-error-overlay] *{box-sizing:border-box;}",
246
+ "[data-forge-preview-error-overlay-panel]{width:min(760px,100%);max-height:calc(100vh - 64px);overflow:auto;border:1px solid rgba(248,113,113,.45);border-radius:8px;background:#111827;box-shadow:0 24px 80px rgba(0,0,0,.42);padding:20px;}",
247
+ "[data-forge-preview-error-overlay-kicker]{margin:0 0 6px;color:#fca5a5;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;}",
248
+ "[data-forge-preview-error-overlay-title]{margin:0 0 16px;font-size:20px;line-height:1.25;font-weight:700;}",
249
+ "[data-forge-preview-error-overlay-list]{display:grid;gap:12px;margin:0;padding:0;list-style:none;}",
250
+ "[data-forge-preview-error-overlay-item]{display:grid;gap:6px;border-top:1px solid rgba(148,163,184,.22);padding-top:12px;}",
251
+ "[data-forge-preview-error-overlay-code]{width:max-content;border-radius:4px;background:rgba(248,113,113,.16);color:#fecaca;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',monospace;font-size:12px;}",
252
+ "[data-forge-preview-error-overlay-message]{margin:0;color:#f8fafc;font-size:14px;line-height:1.5;}",
253
+ "[data-forge-preview-error-overlay-detail]{margin:0;color:#cbd5e1;font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',monospace;font-size:12px;line-height:1.45;}",
254
+ "</style>",
255
+ '<div data-forge-preview-error-overlay role="alert" aria-live="assertive">',
256
+ ' <section data-forge-preview-error-overlay-panel>',
257
+ ' <p data-forge-preview-error-overlay-kicker>Forge preview</p>',
258
+ ' <h1 data-forge-preview-error-overlay-title>Preview refresh failed</h1>',
259
+ ` <ul data-forge-preview-error-overlay-list>${items}</ul>`,
260
+ " </section>",
261
+ "</div>",
262
+ PREVIEW_ERROR_OVERLAY_END,
263
+ ].join("\n");
264
+ }
265
+ function createPreviewErrorOverlayItem(diagnostic) {
266
+ const detail = formatDiagnosticDetail(diagnostic);
267
+ return [
268
+ " <li data-forge-preview-error-overlay-item>",
269
+ ` <code data-forge-preview-error-overlay-code>${escapeHtml(diagnostic.code)}</code>`,
270
+ ` <p data-forge-preview-error-overlay-message>${escapeHtml(diagnostic.message)}</p>`,
271
+ detail === undefined
272
+ ? ""
273
+ : ` <p data-forge-preview-error-overlay-detail>${escapeHtml(detail)}</p>`,
274
+ " </li>",
275
+ ]
276
+ .filter((line) => line.length > 0)
277
+ .join("\n");
278
+ }
279
+ function formatDiagnosticDetail(diagnostic) {
280
+ const parts = [
281
+ getStringDiagnosticProperty(diagnostic, "path"),
282
+ getStringDiagnosticProperty(diagnostic, "importer"),
283
+ getStringDiagnosticProperty(diagnostic, "importSpecifier"),
284
+ getStringDiagnosticProperty(diagnostic, "dependencyName"),
285
+ getStringDiagnosticProperty(diagnostic, "requestedRange"),
286
+ getStringDiagnosticProperty(diagnostic, "script"),
287
+ getStringDiagnosticProperty(diagnostic, "sessionId"),
288
+ formatLimitDiagnostic(diagnostic),
289
+ ].filter((part) => part !== undefined && part.length > 0);
290
+ return parts.length === 0 ? undefined : parts.join(" · ");
291
+ }
292
+ function getStringDiagnosticProperty(diagnostic, key) {
293
+ const value = diagnostic[key];
294
+ return typeof value === "string" ? value : undefined;
295
+ }
296
+ function formatLimitDiagnostic(diagnostic) {
297
+ const limit = diagnostic.limit;
298
+ if (typeof limit !== "object" || limit === null) {
299
+ return undefined;
300
+ }
301
+ const name = limit.name;
302
+ const actual = limit.actual;
303
+ const maximum = limit.maximum;
304
+ if (typeof name !== "string" ||
305
+ typeof actual !== "number" ||
306
+ typeof maximum !== "number") {
307
+ return undefined;
308
+ }
309
+ return `${name}: ${actual}/${maximum}`;
310
+ }
311
+ function removePreviewErrorOverlay(html) {
312
+ return html.replace(new RegExp(`${escapeRegExp(PREVIEW_ERROR_OVERLAY_START)}[\\s\\S]*?${escapeRegExp(PREVIEW_ERROR_OVERLAY_END)}\\n?`, "g"), "");
313
+ }
133
314
  function removeProjectModuleScripts(html) {
134
315
  return html.replace(/<script\b(?=[^>]*\btype\s*=\s*["']module["'])[^>]*>[\s\S]*?<\/script\s*>/gi, "");
135
316
  }
136
317
  function escapeJsonForScript(json) {
137
318
  return json.replace(/</g, "\\u003c");
138
319
  }
320
+ function escapeHtml(value) {
321
+ return value.replace(/[&<>"']/g, (character) => {
322
+ switch (character) {
323
+ case "&":
324
+ return "&amp;";
325
+ case "<":
326
+ return "&lt;";
327
+ case ">":
328
+ return "&gt;";
329
+ case '"':
330
+ return "&quot;";
331
+ case "'":
332
+ return "&#39;";
333
+ default:
334
+ return character;
335
+ }
336
+ });
337
+ }
338
+ function escapeRegExp(value) {
339
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
340
+ }
139
341
  function toBase64(bytes) {
140
342
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
141
343
  let output = "";
@@ -14,10 +14,17 @@ export declare class DefaultDevServerPreview implements ForgeDevServerPreview {
14
14
  stop(sessionId: string): Promise<ForgeDevSessionStopResult>;
15
15
  getSession(sessionId: string): ForgeDevSession | undefined;
16
16
  watch(listener: ForgeDevServerPreviewEventListener): ForgeUnsubscribe;
17
- private validateDevScript;
17
+ private readDevManifest;
18
18
  private failStart;
19
19
  private handleFileEvent;
20
+ private handleWorkspaceChange;
21
+ private wasCssPathBundled;
22
+ private wasWorkspaceModuleBundled;
23
+ private performStyleUpdate;
24
+ private performModuleUpdate;
20
25
  private performRebuild;
26
+ private completeSuccessfulRefresh;
27
+ private completeFailedRebuild;
21
28
  private createPreviewDocument;
22
29
  private allocateSessionId;
23
30
  private emit;
@@ -34,25 +34,35 @@ class DefaultDevServerPreview {
34
34
  },
35
35
  };
36
36
  this.sessions.set(sessionId, record);
37
- const devScriptDiagnostics = await this.validateDevScript();
38
- if (devScriptDiagnostics.length > 0) {
39
- return this.failStart(record, devScriptDiagnostics, "dev-script");
37
+ const devManifestResult = await this.readDevManifest();
38
+ if (!devManifestResult.success) {
39
+ return this.failStart(record, devManifestResult.diagnostics, "dev-script");
40
40
  }
41
+ record.adapter = selectDevServerAdapter(devManifestResult.manifest);
42
+ record.session = {
43
+ ...record.session,
44
+ adapter: record.adapter,
45
+ };
41
46
  const buildResult = await this.options.buildPipeline.build({
42
47
  entry: options.entry,
48
+ signal: options.signal,
43
49
  });
44
50
  if (!buildResult.success) {
45
51
  return this.failStart(record, buildResult.diagnostics, "initial-build", buildResult);
46
52
  }
47
- const previewDocument = await this.createPreviewDocument(sessionId, previewUrl, 1, buildResult);
53
+ const previewDocument = await this.createPreviewDocument(sessionId, previewUrl, 1, buildResult, record.adapter);
54
+ record.lastSuccessfulBuildResult = buildResult;
55
+ record.lastSuccessfulPreviewDocument = previewDocument;
48
56
  const session = {
49
57
  ...record.session,
50
58
  status: "running",
59
+ adapter: record.adapter,
51
60
  revision: 1,
52
61
  updatedAt: this.now(),
53
62
  diagnostics: buildResult.diagnostics,
54
63
  buildResult,
55
64
  previewDocument,
65
+ errorOverlayDocument: undefined,
56
66
  };
57
67
  record.session = session;
58
68
  record.unsubscribe = this.options.fileSystem.watch((event) => {
@@ -72,7 +82,7 @@ class DefaultDevServerPreview {
72
82
  const diagnostic = (0, diagnostics_1.createSessionNotFoundDiagnostic)(sessionId);
73
83
  return { success: false, diagnostics: [diagnostic] };
74
84
  }
75
- return this.performRebuild(record, options.reason ?? "manual", options.entry);
85
+ return this.performRebuild(record, options.reason ?? "manual", options.entry, options.signal);
76
86
  }
77
87
  async stop(sessionId) {
78
88
  const record = this.sessions.get(sessionId);
@@ -87,6 +97,7 @@ class DefaultDevServerPreview {
87
97
  status: "stopped",
88
98
  updatedAt: this.now(),
89
99
  diagnostics: [],
100
+ errorOverlayDocument: undefined,
90
101
  };
91
102
  record.session = session;
92
103
  this.emit({ type: "dev:session-stopped", session });
@@ -105,21 +116,34 @@ class DefaultDevServerPreview {
105
116
  this.listeners.delete(listener);
106
117
  };
107
118
  }
108
- async validateDevScript() {
119
+ async readDevManifest() {
109
120
  try {
110
121
  const manifest = await (0, package_resolution_1.readPackageManifest)(this.options.fileSystem);
111
122
  const devScript = manifest.scripts.dev;
112
123
  if (devScript === undefined) {
113
- return [(0, diagnostics_1.createMissingDevScriptDiagnostic)()];
124
+ return {
125
+ success: false,
126
+ diagnostics: [(0, diagnostics_1.createMissingDevScriptDiagnostic)()],
127
+ };
114
128
  }
115
129
  if (!isSupportedViteDevScript(devScript)) {
116
- return [(0, diagnostics_1.createUnsupportedDevScriptDiagnostic)(devScript)];
130
+ return {
131
+ success: false,
132
+ diagnostics: [(0, diagnostics_1.createUnsupportedDevScriptDiagnostic)(devScript)],
133
+ };
117
134
  }
118
- return [];
135
+ return {
136
+ success: true,
137
+ diagnostics: [],
138
+ manifest,
139
+ };
119
140
  }
120
141
  catch (error) {
121
142
  if (error instanceof package_resolution_1.ForgePackageResolutionError) {
122
- return [error.diagnostic];
143
+ return {
144
+ success: false,
145
+ diagnostics: [error.diagnostic],
146
+ };
123
147
  }
124
148
  throw error;
125
149
  }
@@ -153,13 +177,59 @@ class DefaultDevServerPreview {
153
177
  return;
154
178
  }
155
179
  const previous = record.rebuildQueue ?? Promise.resolve(undefined);
156
- const next = previous.then(() => this.performRebuild(record, change.reason, record.entry));
180
+ const next = previous.then(() => this.handleWorkspaceChange(record, event, change.reason));
157
181
  record.rebuildQueue = next;
158
182
  void next.catch(() => {
159
183
  record.rebuildQueue = undefined;
160
184
  });
161
185
  }
162
- async performRebuild(record, reason, entry) {
186
+ async handleWorkspaceChange(record, event, reason) {
187
+ const cssPath = getUpdatedCssPath(event);
188
+ if (cssPath !== undefined &&
189
+ record.session.status === "running" &&
190
+ this.wasCssPathBundled(record, cssPath)) {
191
+ await this.performStyleUpdate(record, cssPath, reason);
192
+ return;
193
+ }
194
+ const scriptPath = getUpdatedScriptModulePath(event);
195
+ if (scriptPath !== undefined &&
196
+ record.session.status === "running" &&
197
+ this.wasWorkspaceModuleBundled(record, scriptPath)) {
198
+ return this.performModuleUpdate(record, scriptPath, reason);
199
+ }
200
+ return this.performRebuild(record, reason, record.entry);
201
+ }
202
+ wasCssPathBundled(record, path) {
203
+ const styleId = createForgeCssStyleId(path);
204
+ return (record.lastSuccessfulBuildResult?.outputFiles.some((file) => file.contentType === "text/javascript" &&
205
+ file.content.includes(styleId)) ?? false);
206
+ }
207
+ wasWorkspaceModuleBundled(record, path) {
208
+ return (record.lastSuccessfulBuildResult?.browserBundle?.modules.some((moduleRecord) => moduleRecord.origin === "workspace" &&
209
+ moduleRecord.sourcePath === path) ?? false);
210
+ }
211
+ async performStyleUpdate(record, sourcePath, reason) {
212
+ const content = await this.options.fileSystem.readText(sourcePath);
213
+ const session = {
214
+ ...record.session,
215
+ status: "running",
216
+ updatedAt: this.now(),
217
+ errorOverlayDocument: undefined,
218
+ };
219
+ record.session = session;
220
+ this.emit({
221
+ type: "preview:style-updated",
222
+ session,
223
+ styleUpdate: {
224
+ content,
225
+ contentType: "text/css",
226
+ sourcePath,
227
+ styleId: createForgeCssStyleId(sourcePath),
228
+ },
229
+ reason,
230
+ });
231
+ }
232
+ async performModuleUpdate(record, sourcePath, reason) {
163
233
  if (record.session.status === "stopped") {
164
234
  const diagnostic = (0, diagnostics_1.createSessionStoppedDiagnostic)(record.session.id);
165
235
  return {
@@ -168,37 +238,74 @@ class DefaultDevServerPreview {
168
238
  diagnostics: [diagnostic],
169
239
  };
170
240
  }
241
+ const previousBuildResult = record.lastSuccessfulBuildResult;
171
242
  record.session = {
172
243
  ...record.session,
173
244
  status: "rebuilding",
174
245
  updatedAt: this.now(),
246
+ errorOverlayDocument: undefined,
175
247
  };
176
- const buildResult = await this.options.buildPipeline.build({ entry });
248
+ const buildResult = await this.options.buildPipeline.build({
249
+ entry: record.entry,
250
+ });
177
251
  if (!buildResult.success) {
178
- const session = {
179
- ...record.session,
180
- status: "failed",
181
- updatedAt: this.now(),
182
- diagnostics: buildResult.diagnostics,
183
- buildResult,
184
- };
185
- record.session = session;
186
- this.emit({
187
- type: "dev:session-failed",
188
- session,
189
- diagnostics: buildResult.diagnostics,
190
- buildResult,
191
- reason,
192
- });
252
+ return this.completeFailedRebuild(record, reason, buildResult);
253
+ }
254
+ const moduleUpdate = createModuleUpdate(previousBuildResult, buildResult, sourcePath);
255
+ if (moduleUpdate === undefined) {
256
+ return this.completeSuccessfulRefresh(record, reason, buildResult);
257
+ }
258
+ record.lastSuccessfulBuildResult = buildResult;
259
+ const session = {
260
+ ...record.session,
261
+ status: "running",
262
+ updatedAt: this.now(),
263
+ diagnostics: buildResult.diagnostics,
264
+ buildResult,
265
+ previewDocument: record.lastSuccessfulPreviewDocument ?? record.session.previewDocument,
266
+ errorOverlayDocument: undefined,
267
+ };
268
+ record.session = session;
269
+ this.emit({
270
+ type: "preview:module-updated",
271
+ session,
272
+ moduleUpdate,
273
+ buildResult,
274
+ reason,
275
+ });
276
+ return {
277
+ success: true,
278
+ session,
279
+ diagnostics: buildResult.diagnostics,
280
+ buildResult,
281
+ };
282
+ }
283
+ async performRebuild(record, reason, entry, signal) {
284
+ if (record.session.status === "stopped") {
285
+ const diagnostic = (0, diagnostics_1.createSessionStoppedDiagnostic)(record.session.id);
193
286
  return {
194
287
  success: false,
195
- session,
196
- diagnostics: buildResult.diagnostics,
197
- buildResult,
288
+ session: record.session,
289
+ diagnostics: [diagnostic],
198
290
  };
199
291
  }
292
+ record.session = {
293
+ ...record.session,
294
+ status: "rebuilding",
295
+ updatedAt: this.now(),
296
+ errorOverlayDocument: undefined,
297
+ };
298
+ const buildResult = await this.options.buildPipeline.build({ entry, signal });
299
+ if (!buildResult.success) {
300
+ return this.completeFailedRebuild(record, reason, buildResult);
301
+ }
302
+ return this.completeSuccessfulRefresh(record, reason, buildResult);
303
+ }
304
+ async completeSuccessfulRefresh(record, reason, buildResult) {
200
305
  const revision = record.session.revision + 1;
201
- const previewDocument = await this.createPreviewDocument(record.session.id, record.session.previewUrl, revision, buildResult);
306
+ const previewDocument = await this.createPreviewDocument(record.session.id, record.session.previewUrl, revision, buildResult, record.adapter);
307
+ record.lastSuccessfulBuildResult = buildResult;
308
+ record.lastSuccessfulPreviewDocument = previewDocument;
202
309
  const session = {
203
310
  ...record.session,
204
311
  status: "running",
@@ -207,6 +314,7 @@ class DefaultDevServerPreview {
207
314
  diagnostics: buildResult.diagnostics,
208
315
  buildResult,
209
316
  previewDocument,
317
+ errorOverlayDocument: undefined,
210
318
  };
211
319
  record.session = session;
212
320
  this.emit({
@@ -222,11 +330,41 @@ class DefaultDevServerPreview {
222
330
  buildResult,
223
331
  };
224
332
  }
225
- async createPreviewDocument(sessionId, previewUrl, revision, buildResult) {
333
+ completeFailedRebuild(record, reason, buildResult) {
334
+ const previousPreviewDocument = record.lastSuccessfulPreviewDocument ?? record.session.previewDocument;
335
+ const errorOverlayDocument = previousPreviewDocument === undefined
336
+ ? undefined
337
+ : (0, preview_document_1.createForgePreviewErrorOverlayDocument)(previousPreviewDocument, buildResult.diagnostics);
338
+ const session = {
339
+ ...record.session,
340
+ status: "failed",
341
+ updatedAt: this.now(),
342
+ diagnostics: buildResult.diagnostics,
343
+ buildResult,
344
+ previewDocument: previousPreviewDocument,
345
+ errorOverlayDocument,
346
+ };
347
+ record.session = session;
348
+ this.emit({
349
+ type: "dev:session-failed",
350
+ session,
351
+ diagnostics: buildResult.diagnostics,
352
+ buildResult,
353
+ reason,
354
+ });
355
+ return {
356
+ success: false,
357
+ session,
358
+ diagnostics: buildResult.diagnostics,
359
+ buildResult,
360
+ };
361
+ }
362
+ async createPreviewDocument(sessionId, previewUrl, revision, buildResult, adapter) {
226
363
  const htmlShell = (await this.options.fileSystem.exists("/index.html"))
227
364
  ? await this.options.fileSystem.readText("/index.html")
228
365
  : undefined;
229
366
  return (0, preview_document_1.createForgePreviewDocument)({
367
+ adapter,
230
368
  buildResult,
231
369
  htmlShell,
232
370
  previewUrl,
@@ -268,6 +406,30 @@ exports.isSupportedViteDevScript = isSupportedViteDevScript;
268
406
  function createPreviewUrl(sessionId) {
269
407
  return `/preview/${encodeURIComponent(sessionId)}/`;
270
408
  }
409
+ function selectDevServerAdapter(manifest) {
410
+ const dependencies = {
411
+ ...manifest.dependencies,
412
+ ...manifest.devDependencies,
413
+ };
414
+ if ("react" in dependencies || "react-dom" in dependencies) {
415
+ return createDevServerAdapterMetadata("vite-react", "react");
416
+ }
417
+ if ("vue" in dependencies) {
418
+ return createDevServerAdapterMetadata("vite-vue", "vue");
419
+ }
420
+ return createDevServerAdapterMetadata("vite-vanilla", "vanilla");
421
+ }
422
+ function createDevServerAdapterMetadata(name, framework) {
423
+ return {
424
+ framework,
425
+ hmrMode: "hmr-lite",
426
+ name,
427
+ preservesState: false,
428
+ };
429
+ }
430
+ function createForgeCssStyleId(path) {
431
+ return `forge-css:${path}`;
432
+ }
271
433
  function describeFileChange(event) {
272
434
  const path = event.type === "file:renamed" ? event.to : event.path;
273
435
  if (path.startsWith("/.forge/")) {
@@ -277,3 +439,56 @@ function describeFileChange(event) {
277
439
  reason: `${event.type}:${path}`,
278
440
  };
279
441
  }
442
+ function getUpdatedCssPath(event) {
443
+ return event.type === "file:updated" &&
444
+ event.path.endsWith(".css") &&
445
+ !event.path.endsWith(".module.css")
446
+ ? event.path
447
+ : undefined;
448
+ }
449
+ function getUpdatedScriptModulePath(event) {
450
+ return event.type === "file:updated" && isHmrScriptPath(event.path)
451
+ ? event.path
452
+ : undefined;
453
+ }
454
+ function createModuleUpdate(previousBuildResult, buildResult, sourcePath) {
455
+ const previousBundle = previousBuildResult?.browserBundle;
456
+ const nextBundle = buildResult.browserBundle;
457
+ if (previousBundle === undefined ||
458
+ nextBundle === undefined ||
459
+ previousBundle.entry === sourcePath ||
460
+ nextBundle.entry === sourcePath ||
461
+ !sameOutputPaths(previousBundle.outputPaths, nextBundle.outputPaths)) {
462
+ return undefined;
463
+ }
464
+ const previousModule = findWorkspaceModule(previousBundle.modules, sourcePath);
465
+ const nextModule = findWorkspaceModule(nextBundle.modules, sourcePath);
466
+ if (previousModule === undefined ||
467
+ nextModule === undefined ||
468
+ previousModule.outputPath !== nextModule.outputPath) {
469
+ return undefined;
470
+ }
471
+ const outputFile = buildResult.outputFiles.find((file) => file.path === nextModule.outputPath &&
472
+ file.contentType === "text/javascript");
473
+ return outputFile === undefined
474
+ ? undefined
475
+ : {
476
+ content: outputFile.content,
477
+ contentType: "text/javascript",
478
+ outputPath: nextModule.outputPath,
479
+ sourcePath,
480
+ };
481
+ }
482
+ function findWorkspaceModule(modules, sourcePath) {
483
+ return modules.find((moduleRecord) => moduleRecord.origin === "workspace" &&
484
+ moduleRecord.sourcePath === sourcePath);
485
+ }
486
+ function sameOutputPaths(left, right) {
487
+ const sortedLeft = [...left].sort();
488
+ const sortedRight = [...right].sort();
489
+ return (sortedLeft.length === sortedRight.length &&
490
+ sortedLeft.every((path, index) => path === sortedRight[index]));
491
+ }
492
+ function isHmrScriptPath(path) {
493
+ return /\.(?:jsx?|tsx?)$/i.test(path);
494
+ }