@shopify/cli-hydrogen 6.0.2 → 6.1.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.
Files changed (30) hide show
  1. package/dist/commands/hydrogen/deploy.js +72 -8
  2. package/dist/commands/hydrogen/deploy.test.js +111 -9
  3. package/dist/commands/hydrogen/dev.js +33 -23
  4. package/dist/commands/hydrogen/preview.js +20 -10
  5. package/dist/commands/hydrogen/shortcut.js +1 -0
  6. package/dist/commands/hydrogen/upgrade.js +705 -0
  7. package/dist/commands/hydrogen/upgrade.test.js +786 -0
  8. package/dist/generator-templates/starter/CHANGELOG.md +70 -0
  9. package/dist/generator-templates/starter/app/components/Footer.tsx +3 -1
  10. package/dist/generator-templates/starter/app/components/Layout.tsx +13 -10
  11. package/dist/generator-templates/starter/app/routes/[robots.txt].tsx +0 -27
  12. package/dist/generator-templates/starter/package.json +10 -9
  13. package/dist/generator-templates/starter/remix.env.d.ts +2 -0
  14. package/dist/lib/check-lockfile.js +1 -0
  15. package/dist/lib/codegen.js +1 -0
  16. package/dist/lib/flags.js +13 -2
  17. package/dist/lib/log.js +1 -0
  18. package/dist/lib/mini-oxygen/assets.js +118 -0
  19. package/dist/lib/mini-oxygen/common.js +2 -1
  20. package/dist/lib/mini-oxygen/index.js +3 -0
  21. package/dist/lib/mini-oxygen/node.js +15 -3
  22. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +227 -0
  23. package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +200 -0
  24. package/dist/lib/mini-oxygen/workerd-inspector.js +62 -235
  25. package/dist/lib/mini-oxygen/workerd.js +54 -47
  26. package/dist/lib/render-errors.js +2 -0
  27. package/dist/lib/setups/i18n/replacers.test.js +2 -0
  28. package/dist/lib/shell.js +1 -1
  29. package/oclif.manifest.json +90 -8
  30. package/package.json +10 -21
@@ -0,0 +1,227 @@
1
+ import { parse } from 'stack-trace';
2
+
3
+ function addInspectorConsoleLogger(inspector) {
4
+ inspector.ws.addEventListener("message", async (event) => {
5
+ if (typeof event.data !== "string") {
6
+ console.error("Unrecognised devtools event:", event);
7
+ return;
8
+ }
9
+ const evt = JSON.parse(event.data);
10
+ inspector.cleanupMessageQueue(evt);
11
+ if (evt.method === "Runtime.consoleAPICalled") {
12
+ await logConsoleMessage(evt.params, inspector);
13
+ } else if (evt.method === "Runtime.exceptionThrown") {
14
+ console.error(
15
+ await createErrorFromException(evt.params.exceptionDetails, inspector)
16
+ );
17
+ }
18
+ });
19
+ }
20
+ async function createErrorFromException(exceptionDetails, inspector) {
21
+ const errorProperties = {};
22
+ const sourceMapConsumer = await inspector.getSourceMapConsumer();
23
+ if (sourceMapConsumer !== void 0) {
24
+ const message = exceptionDetails.exception?.description?.split("\n")[0];
25
+ const stack = exceptionDetails.stackTrace?.callFrames;
26
+ const formatted = formatStructuredError(sourceMapConsumer, message, stack);
27
+ errorProperties.message = exceptionDetails.text;
28
+ errorProperties.stack = formatted;
29
+ } else {
30
+ errorProperties.message = exceptionDetails.text + " " + (exceptionDetails.exception?.description ?? "");
31
+ }
32
+ return inspector.reconstructError(
33
+ errorProperties,
34
+ exceptionDetails.exception
35
+ );
36
+ }
37
+ async function createErrorFromLog(ro, inspector) {
38
+ if (ro.subtype !== "error" || ro.preview?.subtype !== "error") {
39
+ throw new Error("Not an error object");
40
+ }
41
+ const errorProperties = {
42
+ message: ro.preview.description?.split("\n").filter((line) => !/^\s+at\s/.test(line)).join("\n") ?? ro.preview.properties.find(({ name }) => name === "message")?.value ?? "",
43
+ stack: ro.preview.description ?? ro.description ?? ro.preview.properties.find(({ name }) => name === "stack")?.value,
44
+ cause: ro.preview.properties.find(({ name }) => name === "cause")?.value
45
+ };
46
+ return inspector.reconstructError(errorProperties, ro);
47
+ }
48
+ const mapConsoleAPIMessageTypeToConsoleMethod = {
49
+ log: "log",
50
+ debug: "debug",
51
+ info: "info",
52
+ warning: "warn",
53
+ error: "error",
54
+ dir: "dir",
55
+ dirxml: "dirxml",
56
+ table: "table",
57
+ trace: "trace",
58
+ clear: "clear",
59
+ count: "count",
60
+ assert: "assert",
61
+ profile: "profile",
62
+ profileEnd: "profileEnd",
63
+ timeEnd: "timeEnd",
64
+ startGroup: "group",
65
+ startGroupCollapsed: "groupCollapsed",
66
+ endGroup: "groupEnd"
67
+ };
68
+ async function logConsoleMessage(evt, inspector) {
69
+ const args = [];
70
+ for (const ro of evt.args) {
71
+ switch (ro.type) {
72
+ case "string":
73
+ case "number":
74
+ case "boolean":
75
+ case "undefined":
76
+ case "symbol":
77
+ case "bigint":
78
+ args.push(ro.value);
79
+ break;
80
+ case "function":
81
+ args.push(`[Function: ${ro.description ?? "<no-description>"}]`);
82
+ break;
83
+ case "object":
84
+ if (!ro.preview) {
85
+ args.push(
86
+ ro.subtype === "null" ? "null" : ro.description ?? "<no-description>"
87
+ );
88
+ } else {
89
+ if (ro.preview.description)
90
+ args.push(ro.preview.description);
91
+ switch (ro.preview.subtype) {
92
+ case "array":
93
+ args.push(
94
+ "[ " + ro.preview.properties.map(({ value }) => {
95
+ return value;
96
+ }).join(", ") + (ro.preview.overflow ? "..." : "") + " ]"
97
+ );
98
+ break;
99
+ case "weakmap":
100
+ case "map":
101
+ ro.preview.entries === void 0 ? args.push("{}") : args.push(
102
+ "{\n" + ro.preview.entries.map(({ key, value }) => {
103
+ return ` ${key?.description ?? "<unknown>"} => ${value.description}`;
104
+ }).join(",\n") + (ro.preview.overflow ? "\n ..." : "") + "\n}"
105
+ );
106
+ break;
107
+ case "weakset":
108
+ case "set":
109
+ ro.preview.entries === void 0 ? args.push("{}") : args.push(
110
+ "{ " + ro.preview.entries.map(({ value }) => {
111
+ return `${value.description}`;
112
+ }).join(", ") + (ro.preview.overflow ? ", ..." : "") + " }"
113
+ );
114
+ break;
115
+ case "regexp":
116
+ break;
117
+ case "date":
118
+ break;
119
+ case "generator":
120
+ args.push(ro.preview?.properties[0]?.value || "");
121
+ break;
122
+ case "promise":
123
+ if (ro.preview?.properties[0]?.value === "pending") {
124
+ args.push(`{<${ro.preview.properties[0].value}>}`);
125
+ } else {
126
+ args.push(
127
+ `{<${ro.preview?.properties[0]?.value}>: ${ro.preview?.properties[1]?.value}}`
128
+ );
129
+ }
130
+ break;
131
+ case "node":
132
+ case "iterator":
133
+ case "proxy":
134
+ case "typedarray":
135
+ case "arraybuffer":
136
+ case "dataview":
137
+ case "webassemblymemory":
138
+ case "wasmvalue":
139
+ break;
140
+ case "error":
141
+ const error = await createErrorFromLog(ro, inspector);
142
+ args.splice(-1, 1, error);
143
+ break;
144
+ default:
145
+ args.push(
146
+ "{\n" + ro.preview.properties.map(({ name, value }) => {
147
+ return ` ${name}: ${value}`;
148
+ }).join(",\n") + (ro.preview.overflow ? "\n ..." : "") + "\n}"
149
+ );
150
+ }
151
+ }
152
+ break;
153
+ default:
154
+ args.push(ro.description || ro.unserializableValue || "\u{1F98B}");
155
+ break;
156
+ }
157
+ }
158
+ const method = mapConsoleAPIMessageTypeToConsoleMethod[evt.type];
159
+ if (method in console) {
160
+ switch (method) {
161
+ case "dir":
162
+ console.dir(args);
163
+ break;
164
+ case "table":
165
+ console.table(args);
166
+ break;
167
+ default:
168
+ console[method].apply(console, args);
169
+ break;
170
+ }
171
+ } else {
172
+ console.warn(`Unsupported console method: ${method}`);
173
+ console.warn("console event:", evt);
174
+ }
175
+ }
176
+ function formatStructuredError(sourceMapConsumer, message, frames) {
177
+ const lines = [];
178
+ if (message !== void 0)
179
+ lines.push(message);
180
+ frames?.forEach(({ functionName, lineNumber, columnNumber }, i) => {
181
+ try {
182
+ if (lineNumber) {
183
+ const pos = sourceMapConsumer.originalPositionFor({
184
+ line: lineNumber + 1,
185
+ column: columnNumber
186
+ });
187
+ if (i === 0 && pos.source && pos.line) {
188
+ const fileSource = sourceMapConsumer.sourceContentFor(pos.source);
189
+ const fileSourceLine = fileSource?.split("\n")[pos.line - 1] || "";
190
+ lines.push(fileSourceLine.trim());
191
+ if (pos.column) {
192
+ lines.push(
193
+ `${" ".repeat(pos.column - fileSourceLine.search(/\S/))}^`
194
+ );
195
+ }
196
+ }
197
+ if (pos && pos.line !== null && pos.column !== null) {
198
+ const convertedFnName = pos.name || functionName || "";
199
+ let convertedLocation = `${pos.source}:${pos.line}:${pos.column + 1}`;
200
+ if (convertedFnName === "") {
201
+ lines.push(` at ${convertedLocation}`);
202
+ } else {
203
+ lines.push(` at ${convertedFnName} (${convertedLocation})`);
204
+ }
205
+ }
206
+ }
207
+ } catch {
208
+ }
209
+ });
210
+ return lines.join("\n");
211
+ }
212
+ function formatStack(sourceMapConsumer, stack) {
213
+ const message = stack.split("\n")[0];
214
+ const callSites = parse({ stack });
215
+ const frames = callSites.map((site) => ({
216
+ functionName: site.getFunctionName() ?? "",
217
+ // `Protocol.Runtime.CallFrame`s line numbers are 0-indexed, hence `- 1`
218
+ lineNumber: (site.getLineNumber() ?? 1) - 1,
219
+ columnNumber: site.getColumnNumber() ?? 1,
220
+ // Unused by `formattedError`
221
+ scriptId: "",
222
+ url: ""
223
+ }));
224
+ return formatStructuredError(sourceMapConsumer, message, frames);
225
+ }
226
+
227
+ export { addInspectorConsoleLogger, createErrorFromException, createErrorFromLog, formatStack, formatStructuredError };
@@ -0,0 +1,200 @@
1
+ import crypto from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { createServer } from 'node:http';
4
+ import { WebSocketServer } from 'ws';
5
+ import { request } from 'undici';
6
+
7
+ const CFW_DEVTOOLS = "https://devtools.devprod.cloudflare.dev";
8
+ const H2_FAVICON_URL = "https://cdn.shopify.com/s/files/1/0598/4822/8886/files/favicon.svg";
9
+ function createInspectorProxy(port, sourceFilePath, newInspectorConnection) {
10
+ const sessionId = crypto.randomUUID();
11
+ let debuggerWs = void 0;
12
+ let inspector = newInspectorConnection;
13
+ let isDevToolsInBrowser = false;
14
+ const sourceMapPathname = "/__index.js.map";
15
+ const sourceMapURL = `http://localhost:${port}${sourceMapPathname}`;
16
+ const server = createServer((req, res) => {
17
+ const [url = "/", queryString = ""] = req.url?.split("?") || [];
18
+ switch (url) {
19
+ case "/json/version":
20
+ res.setHeader("Content-Type", "application/json");
21
+ res.end(
22
+ JSON.stringify({ Browser: "hydrogen/v2", "Protocol-Version": "1.3" })
23
+ );
24
+ break;
25
+ case "/json":
26
+ case "/json/list":
27
+ {
28
+ res.setHeader("Content-Type", "application/json");
29
+ const localHost = `localhost:${port}/ws`;
30
+ const devtoolsFrontendUrl = `devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=${localHost}`;
31
+ const devtoolsFrontendUrlCompat = `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${localHost}`;
32
+ res.end(
33
+ JSON.stringify([
34
+ {
35
+ id: sessionId,
36
+ type: "node",
37
+ webSocketDebuggerUrl: `ws://${localHost}`,
38
+ devtoolsFrontendUrl,
39
+ devtoolsFrontendUrlCompat,
40
+ // Below are fields that are visible in the DevTools UI.
41
+ title: "Hydrogen / Oxygen Worker",
42
+ faviconUrl: H2_FAVICON_URL,
43
+ url: "https://" + new URL(inspector.ws.url).host
44
+ }
45
+ ])
46
+ );
47
+ }
48
+ return;
49
+ case sourceMapPathname:
50
+ res.setHeader("Content-Type", "text/plain");
51
+ res.setHeader("Cache-Control", "no-store");
52
+ res.setHeader(
53
+ "Access-Control-Allow-Origin",
54
+ req.headers.origin ?? "devtools://devtools"
55
+ );
56
+ res.end(readFileSync(sourceFilePath + ".map", "utf-8"));
57
+ break;
58
+ case "/favicon.ico":
59
+ proxyHttp(H2_FAVICON_URL, req.headers, res);
60
+ break;
61
+ case "/":
62
+ if (!queryString) {
63
+ res.statusCode = 302;
64
+ res.setHeader(
65
+ "Location",
66
+ `/?experiments=true&v8only=true&debugger=true&ws=localhost:${port}/ws`
67
+ );
68
+ res.end();
69
+ } else {
70
+ proxyHttp(
71
+ CFW_DEVTOOLS + "/js_app",
72
+ req.headers,
73
+ res,
74
+ (content) => (
75
+ // HTML from DevTools comes without closing <body> and <html> tags.
76
+ // The browser closes them automatically, then modifies the DOM with JS.
77
+ // This adds a loading indicator before the JS kicks in and modifies the DOM.
78
+ content + '<div style="display: flex; flex-direction: column; align-items: center; padding-top: 20px; font-family: Arial; color: white">Loading DevTools...</div></body></html>'
79
+ )
80
+ );
81
+ }
82
+ break;
83
+ default:
84
+ if (url === "/panels/sources/sources-meta.js" || url.startsWith("/core/i18n/locales/") && url.endsWith(".json")) {
85
+ proxyHttp(
86
+ CFW_DEVTOOLS + url,
87
+ req.headers,
88
+ res,
89
+ (content) => content.replace(/['"]Cloudflare['"]/g, '"Hydrogen"')
90
+ );
91
+ } else {
92
+ proxyHttp(CFW_DEVTOOLS + url, req.headers, res);
93
+ }
94
+ break;
95
+ }
96
+ });
97
+ const wsServer = new WebSocketServer({ server, clientTracking: true });
98
+ server.listen(port);
99
+ let messageBuffer = [];
100
+ wsServer.on("connection", (ws, req) => {
101
+ if (wsServer.clients.size > 1) {
102
+ console.error(
103
+ "Tried to open a new devtools window when a previous one was already open."
104
+ );
105
+ ws.close(1013, "Too many clients; only one can be connected at a time");
106
+ } else {
107
+ inspector.ws.send(
108
+ JSON.stringify({ id: 1e8, method: "Debugger.disable" })
109
+ );
110
+ debuggerWs?.removeEventListener("message", sendMessageToInspector);
111
+ debuggerWs = ws;
112
+ isDevToolsInBrowser = /mozilla/i.test(req.headers["user-agent"] ?? "");
113
+ debuggerWs.addEventListener("message", sendMessageToInspector);
114
+ debuggerWs.addEventListener("close", () => {
115
+ debuggerWs?.removeEventListener("message", sendMessageToInspector);
116
+ debuggerWs = void 0;
117
+ });
118
+ messageBuffer.forEach(sendMessageToDebugger);
119
+ messageBuffer = [];
120
+ }
121
+ });
122
+ if (inspector.ws)
123
+ onInspectorConnection();
124
+ function onInspectorConnection() {
125
+ inspector.ws.addEventListener("message", sendMessageToDebugger);
126
+ debuggerWs?.send(
127
+ JSON.stringify({
128
+ method: "Runtime.consoleAPICalled",
129
+ params: {
130
+ type: "warning",
131
+ args: [
132
+ {
133
+ type: "string",
134
+ value: "Source code changed. Please reload the DevTools to reconnect the debugger."
135
+ }
136
+ ],
137
+ executionContextId: Date.now(),
138
+ timestamp: Date.now()
139
+ }
140
+ })
141
+ );
142
+ debuggerWs?.close(1001, "Source code changed");
143
+ }
144
+ function sendMessageToInspector(event) {
145
+ inspector.ws.send(event.data);
146
+ }
147
+ function sendMessageToDebugger(event) {
148
+ if (isDevToolsInBrowser) {
149
+ event = enhanceDevToolsEvent(event, sourceMapURL);
150
+ }
151
+ if (debuggerWs) {
152
+ debuggerWs.send(event.data);
153
+ } else {
154
+ messageBuffer.push(event);
155
+ }
156
+ }
157
+ return {
158
+ updateInspectorConnection(newConnection) {
159
+ inspector = newConnection;
160
+ onInspectorConnection();
161
+ }
162
+ };
163
+ }
164
+ function enhanceDevToolsEvent(event, sourceMapUrl) {
165
+ const message = JSON.parse(event.data);
166
+ if (message.method === "Debugger.scriptParsed") {
167
+ if (message.params.sourceMapURL === "index.js.map") {
168
+ message.params.sourceMapURL = sourceMapUrl;
169
+ }
170
+ }
171
+ return { ...event, data: JSON.stringify(message) };
172
+ }
173
+ function proxyHttp(url, originalHeaders, nodeResponse, contentReplacer) {
174
+ const headers = Object.fromEntries(Object.entries(originalHeaders));
175
+ delete headers["host"];
176
+ delete headers["cookie"];
177
+ if (contentReplacer)
178
+ delete headers["accept-encoding"];
179
+ return request(url, { responseHeader: "raw", headers }).then((response) => {
180
+ nodeResponse.statusCode = response.statusCode;
181
+ if (nodeResponse.statusCode === 404) {
182
+ return nodeResponse.end("Not found");
183
+ }
184
+ Object.entries(response.headers).forEach(([key, value]) => {
185
+ if (value) {
186
+ nodeResponse.setHeader(key, value);
187
+ }
188
+ });
189
+ if (contentReplacer) {
190
+ return response.body?.text().then(contentReplacer).then(nodeResponse.end.bind(nodeResponse));
191
+ }
192
+ return response.body.pipe(nodeResponse);
193
+ }).catch((err) => {
194
+ console.error(err);
195
+ nodeResponse.statusCode = 500;
196
+ nodeResponse.end("Internal error");
197
+ });
198
+ }
199
+
200
+ export { createInspectorProxy };