@shopify/cli-hydrogen 6.0.2 → 7.0.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 (108) hide show
  1. package/dist/commands/hydrogen/build.js +40 -78
  2. package/dist/commands/hydrogen/codegen.js +8 -3
  3. package/dist/commands/hydrogen/deploy.js +173 -37
  4. package/dist/commands/hydrogen/deploy.test.js +192 -20
  5. package/dist/commands/hydrogen/dev.js +56 -31
  6. package/dist/commands/hydrogen/init.js +1 -1
  7. package/dist/commands/hydrogen/init.test.js +155 -53
  8. package/dist/commands/hydrogen/link.js +5 -21
  9. package/dist/commands/hydrogen/link.test.js +10 -10
  10. package/dist/commands/hydrogen/preview.js +22 -11
  11. package/dist/commands/hydrogen/setup.js +0 -4
  12. package/dist/commands/hydrogen/setup.test.js +0 -1
  13. package/dist/commands/hydrogen/shortcut.js +1 -0
  14. package/dist/commands/hydrogen/upgrade.js +720 -0
  15. package/dist/commands/hydrogen/upgrade.test.js +786 -0
  16. package/dist/generator-templates/starter/.graphqlrc.yml +12 -1
  17. package/dist/generator-templates/starter/CHANGELOG.md +126 -0
  18. package/dist/generator-templates/starter/README.md +23 -0
  19. package/dist/generator-templates/starter/app/components/Cart.tsx +1 -1
  20. package/dist/generator-templates/starter/app/components/Footer.tsx +3 -1
  21. package/dist/generator-templates/starter/app/components/Header.tsx +5 -1
  22. package/dist/generator-templates/starter/app/components/Layout.tsx +14 -11
  23. package/dist/generator-templates/starter/app/components/Search.tsx +1 -1
  24. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
  25. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +39 -0
  26. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
  27. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
  28. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
  29. package/dist/generator-templates/starter/app/lib/fragments.ts +102 -0
  30. package/dist/generator-templates/starter/app/lib/session.ts +67 -0
  31. package/dist/generator-templates/starter/app/root.tsx +11 -45
  32. package/dist/generator-templates/starter/app/routes/[robots.txt].tsx +0 -27
  33. package/dist/generator-templates/starter/app/routes/account.$.tsx +8 -4
  34. package/dist/generator-templates/starter/app/routes/account._index.tsx +5 -0
  35. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +215 -206
  36. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +56 -163
  37. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +32 -109
  38. package/dist/generator-templates/starter/app/routes/account.profile.tsx +40 -180
  39. package/dist/generator-templates/starter/app/routes/account.tsx +20 -135
  40. package/dist/generator-templates/starter/app/routes/account_.authorize.tsx +5 -0
  41. package/dist/generator-templates/starter/app/routes/account_.login.tsx +3 -140
  42. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +5 -24
  43. package/dist/generator-templates/starter/app/routes/cart.tsx +7 -5
  44. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -1
  45. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +2 -2
  46. package/dist/generator-templates/starter/app/routes/search.tsx +1 -1
  47. package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +506 -0
  48. package/dist/generator-templates/starter/package.json +11 -10
  49. package/dist/generator-templates/starter/remix.config.js +4 -0
  50. package/dist/generator-templates/starter/remix.env.d.ts +6 -11
  51. package/dist/generator-templates/starter/server.ts +24 -167
  52. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +104 -881
  53. package/dist/hooks/init.js +4 -4
  54. package/dist/lib/auth.js +5 -10
  55. package/dist/lib/build.js +6 -1
  56. package/dist/lib/bundle/analyzer.js +36 -26
  57. package/dist/lib/check-lockfile.js +1 -0
  58. package/dist/lib/codegen.js +59 -18
  59. package/dist/lib/defer.js +12 -0
  60. package/dist/lib/file.js +52 -3
  61. package/dist/lib/flags.js +27 -9
  62. package/dist/lib/get-oxygen-deployment-data.test.js +4 -2
  63. package/dist/lib/graphql/admin/client.test.js +2 -2
  64. package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
  65. package/dist/lib/log.js +32 -14
  66. package/dist/lib/mini-oxygen/assets.js +118 -0
  67. package/dist/lib/mini-oxygen/common.js +2 -1
  68. package/dist/lib/mini-oxygen/index.js +7 -5
  69. package/dist/lib/mini-oxygen/mini-oxygen.test.js +214 -0
  70. package/dist/lib/mini-oxygen/node.js +19 -5
  71. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +227 -0
  72. package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +200 -0
  73. package/dist/lib/mini-oxygen/workerd-inspector.js +62 -235
  74. package/dist/lib/mini-oxygen/workerd.js +74 -50
  75. package/dist/lib/missing-routes.js +6 -3
  76. package/dist/lib/onboarding/common.js +40 -9
  77. package/dist/lib/onboarding/local.js +19 -11
  78. package/dist/lib/onboarding/remote.js +48 -28
  79. package/dist/lib/render-errors.js +2 -0
  80. package/dist/lib/request-events.js +65 -31
  81. package/dist/lib/setups/css/assets.js +1 -46
  82. package/dist/lib/setups/css/css-modules.js +3 -2
  83. package/dist/lib/setups/css/postcss.js +4 -2
  84. package/dist/lib/setups/css/tailwind.js +4 -2
  85. package/dist/lib/setups/css/vanilla-extract.js +3 -2
  86. package/dist/lib/setups/i18n/replacers.test.js +56 -38
  87. package/dist/lib/shell.js +1 -1
  88. package/dist/lib/template-diff.js +89 -0
  89. package/dist/lib/template-downloader.js +3 -2
  90. package/dist/lib/transpile/project.js +1 -1
  91. package/dist/virtual-routes/assets/debug-network.css +592 -0
  92. package/dist/virtual-routes/assets/favicon-dark.svg +20 -0
  93. package/dist/virtual-routes/components/FlameChartWrapper.jsx +8 -10
  94. package/dist/virtual-routes/components/IconClose.jsx +38 -0
  95. package/dist/virtual-routes/components/IconDiscard.jsx +44 -0
  96. package/dist/virtual-routes/components/RequestDetails.jsx +179 -0
  97. package/dist/virtual-routes/components/RequestTable.jsx +92 -0
  98. package/dist/virtual-routes/components/RequestWaterfall.jsx +151 -0
  99. package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +176 -0
  100. package/dist/virtual-routes/routes/subrequest-profiler.jsx +243 -0
  101. package/oclif.manifest.json +134 -59
  102. package/package.json +18 -26
  103. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +0 -161
  104. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +0 -129
  105. package/dist/generator-templates/starter/app/routes/account_.register.tsx +0 -207
  106. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +0 -136
  107. package/dist/virtual-routes/routes/debug-network.jsx +0 -289
  108. /package/dist/generator-templates/starter/app/{utils.ts → lib/variants.ts} +0 -0
@@ -0,0 +1,118 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createServer } from 'node:http';
4
+ import { lookupMimeType } from '@shopify/cli-kit/node/mimes';
5
+
6
+ const html = String.raw;
7
+ const artificialAssetPrefix = "mini-oxygen/00000/11111/22222/33333";
8
+ function buildAssetsUrl(assetsPort) {
9
+ return `http://localhost:${assetsPort}/${artificialAssetPrefix}/`;
10
+ }
11
+ function createAssetsServer(buildPathClient) {
12
+ return createServer(async (req, res) => {
13
+ res.setHeader("Access-Control-Allow-Origin", "*");
14
+ res.setHeader("X-Content-Type-Options", "nosniff");
15
+ const pathname = req.url?.split("?")[0] || "";
16
+ const isValidAssetPath = pathname.startsWith(`/${artificialAssetPrefix}/`) && !pathname.includes("..");
17
+ const relativeAssetPath = isValidAssetPath ? pathname.replace(`/${artificialAssetPrefix}`, "") : pathname;
18
+ if (isValidAssetPath) {
19
+ const filePath = path.join(buildPathClient, relativeAssetPath);
20
+ const file = await fs.open(filePath).catch(() => {
21
+ });
22
+ const stat = await file?.stat().catch(() => {
23
+ });
24
+ if (file && stat?.isFile()) {
25
+ res.setHeader("Content-Length", stat.size);
26
+ res.setHeader(
27
+ "Content-Type",
28
+ lookupMimeType(filePath) || "application/octet-stream"
29
+ );
30
+ return file.createReadStream().pipe(res);
31
+ }
32
+ }
33
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
34
+ res.writeHead(404);
35
+ res.end(
36
+ html`<html>
37
+ <head>
38
+ <title>404: Page not found</title>
39
+ </head>
40
+ <body
41
+ style="display: flex; flex-direction: column; align-items: center; padding-top: 20px; font-family: Arial"
42
+ >
43
+ <h2>404 NOT FOUND</h2>
44
+ <p>
45
+ ${isValidAssetPath ? "This file was not found in the build output directory:" : "The following URL pathname is not valid:"}
46
+ </p>
47
+ <pre>${relativeAssetPath}</pre>
48
+ </body>
49
+ </html>`
50
+ );
51
+ });
52
+ }
53
+ const STATIC_ASSET_EXTENSIONS = Object.freeze([
54
+ "7Z",
55
+ "CSV",
56
+ "GIF",
57
+ "MIDI",
58
+ "PNG",
59
+ "TIF",
60
+ "ZIP",
61
+ "AVI",
62
+ "DOC",
63
+ "GZ",
64
+ "MKV",
65
+ "PPT",
66
+ "TIFF",
67
+ "ZST",
68
+ "AVIF",
69
+ "DOCX",
70
+ "ICO",
71
+ "MP3",
72
+ "PPTX",
73
+ "TTF",
74
+ "APK",
75
+ "DMG",
76
+ "ISO",
77
+ "MP4",
78
+ "PS",
79
+ "WEBM",
80
+ "BIN",
81
+ "EJS",
82
+ "JAR",
83
+ "OGG",
84
+ "RAR",
85
+ "WEBP",
86
+ "BMP",
87
+ "EOT",
88
+ "JPG",
89
+ "OTF",
90
+ "SVG",
91
+ "WOFF",
92
+ "BZ2",
93
+ "EPS",
94
+ "JPEG",
95
+ "PDF",
96
+ "SVGZ",
97
+ "WOFF2",
98
+ "CLASS",
99
+ "EXE",
100
+ "JS",
101
+ "PICT",
102
+ "SWF",
103
+ "XLS",
104
+ "CSS",
105
+ "FLAC",
106
+ "MID",
107
+ "PLS",
108
+ "TAR",
109
+ "XLSX",
110
+ "TXT",
111
+ "XML",
112
+ "MAP",
113
+ "HTML",
114
+ "GLB",
115
+ "JSON"
116
+ ]);
117
+
118
+ export { STATIC_ASSET_EXTENSIONS, buildAssetsUrl, createAssetsServer };
@@ -2,6 +2,7 @@ import { outputToken, outputInfo, outputContent } from '@shopify/cli-kit/node/ou
2
2
  import colors from '@shopify/cli-kit/node/colors';
3
3
  import { DEV_ROUTES } from '../request-events.js';
4
4
 
5
+ const DEFAULT_INSPECTOR_PORT = 9229;
5
6
  function logRequestLine(request, {
6
7
  responseStatus = 200,
7
8
  durationMs = 0
@@ -55,4 +56,4 @@ const OXYGEN_HEADERS_MAP = {
55
56
  }
56
57
  };
57
58
 
58
- export { OXYGEN_HEADERS_MAP, logRequestLine };
59
+ export { DEFAULT_INSPECTOR_PORT, OXYGEN_HEADERS_MAP, logRequestLine };
@@ -1,12 +1,14 @@
1
- async function startMiniOxygen(options, useWorkerd = false) {
2
- if (useWorkerd) {
3
- const { startWorkerdServer } = await import('./workerd.js');
4
- return startWorkerdServer(options);
5
- } else {
1
+ export { DEFAULT_INSPECTOR_PORT } from './common.js';
2
+ export { buildAssetsUrl } from './assets.js';
3
+
4
+ async function startMiniOxygen(options, useNodeRuntime = false) {
5
+ if (useNodeRuntime) {
6
6
  process.env.MINIFLARE_SUBREQUEST_LIMIT = 100;
7
7
  const { startNodeServer } = await import('./node.js');
8
8
  return startNodeServer(options);
9
9
  }
10
+ const { startWorkerdServer } = await import('./workerd.js');
11
+ return startWorkerdServer(options);
10
12
  }
11
13
 
12
14
  export { startMiniOxygen };
@@ -0,0 +1,214 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { transformWithEsbuild } from 'vite';
3
+ import { startMiniOxygen, buildAssetsUrl } from './index.js';
4
+ import { joinPath } from '@shopify/cli-kit/node/path';
5
+ import { inTemporaryDirectory, removeFile, touchFile, writeFile } from '@shopify/cli-kit/node/fs';
6
+ import getPort from 'get-port';
7
+
8
+ describe("MiniOxygen Worker Runtime", () => {
9
+ it("receives HTML from test worker", async () => {
10
+ await withFixtures(
11
+ async ({ writeHandler }) => {
12
+ await writeHandler(() => {
13
+ return new Response("<html><body>Hello, world</body></html>", {
14
+ headers: { "content-type": "text/html" }
15
+ });
16
+ });
17
+ },
18
+ async ({ fetch: fetch2 }) => {
19
+ const response = await fetch2("/");
20
+ expect(response.headers.get("content-type")).toEqual("text/html");
21
+ await expect(response.text()).resolves.toEqual(
22
+ "<html><body>Hello, world</body></html>"
23
+ );
24
+ }
25
+ );
26
+ });
27
+ it("reloads script", async () => {
28
+ await withFixtures(
29
+ async ({ writeHandler }) => {
30
+ await writeHandler((req, env) => new Response("foo"));
31
+ },
32
+ async ({ fetch: fetch2, writeHandler, miniOxygen }) => {
33
+ let response = await fetch2("/");
34
+ await expect(response.text()).resolves.toEqual("foo");
35
+ await writeHandler((req, env) => new Response("bar"));
36
+ await miniOxygen.reload();
37
+ response = await fetch2("/");
38
+ await expect(response.text()).resolves.toEqual("bar");
39
+ }
40
+ );
41
+ });
42
+ it("reloads environment variables", async () => {
43
+ await withFixtures(
44
+ async ({ writeHandler }) => {
45
+ await writeHandler((req, env) => new Response(env.TEST));
46
+ return { env: { TEST: "foo" } };
47
+ },
48
+ async ({ fetch: fetch2, miniOxygen }) => {
49
+ let response = await fetch2("/");
50
+ await expect(response.text()).resolves.toEqual("foo");
51
+ await miniOxygen.reload({ env: { TEST: "bar" } });
52
+ response = await fetch2("/");
53
+ await expect(response.text()).resolves.toEqual("bar");
54
+ }
55
+ );
56
+ });
57
+ it("serves a static asset via proxy", async () => {
58
+ await withFixtures(
59
+ async ({ writeHandler, writeAsset }) => {
60
+ await writeAsset(
61
+ "star.svg",
62
+ '<svg><polygon points="100,10 40,198 190,78 10,78 160,198" style="fill:gold;"/></svg>'
63
+ );
64
+ await writeHandler(() => new Response("ok"));
65
+ },
66
+ async ({ fetch: fetch2, fetchAsset }) => {
67
+ const asset = await (await fetchAsset("/star.svg")).text();
68
+ expect(asset).toEqual(
69
+ '<svg><polygon points="100,10 40,198 190,78 10,78 160,198" style="fill:gold;"/></svg>'
70
+ );
71
+ const response = await fetch2("/star.svg");
72
+ const result = await response.text();
73
+ expect(response.headers.get("content-type")).toEqual("image/svg+xml");
74
+ expect(result).toEqual(asset);
75
+ }
76
+ );
77
+ });
78
+ it("adds Oxygen request headers", async () => {
79
+ await withFixtures(
80
+ async ({ writeHandler }) => {
81
+ await writeHandler(
82
+ (req) => new Response(
83
+ JSON.stringify(Object.fromEntries(req.headers.entries())),
84
+ { headers: { "content-type": "application/json" } }
85
+ )
86
+ );
87
+ },
88
+ async ({ fetch: fetch2 }) => {
89
+ const response = await fetch2("/");
90
+ await expect(response.json()).resolves.toMatchObject({
91
+ "request-id": expect.stringMatching(/^[a-z0-9-]{36}$/),
92
+ "oxygen-buyer-ip": "127.0.0.1"
93
+ });
94
+ }
95
+ );
96
+ });
97
+ it("applies sourcemaps to error stack traces", async () => {
98
+ await withFixtures(
99
+ async ({ writeHandler }) => {
100
+ await writeHandler(
101
+ () => {
102
+ function doStuff() {
103
+ throw new Error("test");
104
+ }
105
+ try {
106
+ doStuff();
107
+ } catch (error) {
108
+ console.error(error);
109
+ throw error;
110
+ }
111
+ return new Response("ok");
112
+ },
113
+ { sourcemap: true }
114
+ );
115
+ },
116
+ async ({ fetch: fetch2, miniOxygen, miniOxygenOptions }) => {
117
+ const spy = vi.spyOn(console, "error").mockImplementation((error) => {
118
+ });
119
+ const response = await fetch2("/");
120
+ expect(response.status).toEqual(500);
121
+ await expect(response.text()).resolves.toEqual("Error: test");
122
+ await vi.waitFor(
123
+ () => expect(spy.mock.calls.length).toBeGreaterThan(1)
124
+ // At least 2 calls
125
+ );
126
+ expect(spy, "Logged with sourcemaps").toHaveBeenCalledWith(
127
+ expect.objectContaining({
128
+ stack: expect.stringMatching(
129
+ // Shows `doStuff` and the offending line by mapping
130
+ // the minified code with sourcemaps:
131
+ /Error: test\nthrow new Error\("test"\);\n.*at doStuff \(/s
132
+ )
133
+ })
134
+ );
135
+ expect(spy).toHaveBeenCalledWith(new Error("test"));
136
+ spy.mockClear();
137
+ await removeFile(miniOxygenOptions.buildPathWorkerFile + ".map");
138
+ await miniOxygen.reload();
139
+ await fetch2("/");
140
+ await vi.waitFor(
141
+ () => expect(spy.mock.calls.length).toBeGreaterThan(1)
142
+ // At least 2 calls
143
+ );
144
+ expect(spy, "Logged without sourcemaps").toHaveBeenCalledWith(
145
+ expect.objectContaining({
146
+ stack: expect.stringMatching(
147
+ // Doesn't show `doStuff` because it's minified
148
+ /Error: test\n\s+at \w .*at Object\.fetch/s
149
+ )
150
+ })
151
+ );
152
+ expect(spy).toHaveBeenCalledWith(new Error("test"));
153
+ spy.mockRestore();
154
+ }
155
+ );
156
+ });
157
+ });
158
+ function withFixtures(setup, runTest) {
159
+ return inTemporaryDirectory(async (tmpDir) => {
160
+ const relativeDistClient = joinPath("dist", "client");
161
+ const relativeDistWorker = joinPath("dist", "worker");
162
+ const relativeWorkerEntry = joinPath(relativeDistWorker, "index.js");
163
+ const writeFixture = async (filename, content) => {
164
+ const filepath = joinPath(tmpDir, filename);
165
+ await touchFile(filepath);
166
+ await writeFile(filepath, content);
167
+ };
168
+ const writeAsset = (filepath, content) => writeFixture(joinPath(relativeDistClient, filepath), content);
169
+ const writeHandler = async (handler, { sourcemap = false } = {}) => {
170
+ let code = `export default { fetch: ${handler.toString()} }`;
171
+ if (sourcemap) {
172
+ const result = await transformWithEsbuild(code, relativeWorkerEntry, {
173
+ minify: true,
174
+ sourcemap: true
175
+ });
176
+ code = result.code;
177
+ await writeFixture(
178
+ relativeWorkerEntry + ".map",
179
+ JSON.stringify(result.map)
180
+ );
181
+ }
182
+ await writeFixture(relativeWorkerEntry, code);
183
+ };
184
+ const optionsFromSetup = await setup({
185
+ writeFixture,
186
+ writeAsset,
187
+ writeHandler
188
+ });
189
+ const miniOxygenOptions = {
190
+ root: tmpDir,
191
+ port: await getPort(),
192
+ buildPathWorkerFile: joinPath(tmpDir, relativeWorkerEntry),
193
+ buildPathClient: joinPath(tmpDir, relativeDistClient),
194
+ inspectorPort: 9229,
195
+ assetsPort: 1347,
196
+ env: {},
197
+ ...optionsFromSetup
198
+ };
199
+ const miniOxygen = await startMiniOxygen(miniOxygenOptions);
200
+ try {
201
+ await runTest({
202
+ writeFixture,
203
+ writeHandler,
204
+ writeAsset,
205
+ miniOxygen,
206
+ miniOxygenOptions,
207
+ fetch: (pathname) => fetch(miniOxygen.listeningAt + pathname),
208
+ fetchAsset: (pathname) => fetch(buildAssetsUrl(miniOxygenOptions.assetsPort) + pathname)
209
+ });
210
+ } finally {
211
+ await miniOxygen.close();
212
+ }
213
+ });
214
+ }
@@ -2,23 +2,27 @@ import { randomUUID } from 'node:crypto';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { readFile } from '@shopify/cli-kit/node/fs';
4
4
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
5
- import { startServer, Request } from '@shopify/mini-oxygen';
5
+ import { Response, startServer, Request } from '@shopify/mini-oxygen';
6
6
  import { DEFAULT_PORT } from '../flags.js';
7
7
  import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
8
- import { handleDebugNetworkRequest, H2O_BINDING_NAME, logRequestEvent } from '../request-events.js';
8
+ import { setConstructors, createLogRequestEvent, handleDebugNetworkRequest, H2O_BINDING_NAME } from '../request-events.js';
9
9
 
10
10
  async function startNodeServer({
11
11
  port = DEFAULT_PORT,
12
12
  watch = false,
13
13
  buildPathWorkerFile,
14
14
  buildPathClient,
15
- env
15
+ env,
16
+ debug = false,
17
+ inspectorPort
16
18
  }) {
17
19
  const oxygenHeaders = Object.fromEntries(
18
20
  Object.entries(OXYGEN_HEADERS_MAP).map(([key, value]) => {
19
21
  return [key, value.defaultValue];
20
22
  })
21
23
  );
24
+ setConstructors({ Response });
25
+ const logRequestEvent = createLogRequestEvent();
22
26
  const asyncLocalStorage = new AsyncLocalStorage();
23
27
  const serviceBindings = {
24
28
  [H2O_BINDING_NAME]: {
@@ -33,6 +37,9 @@ async function startNodeServer({
33
37
  )
34
38
  }
35
39
  };
40
+ if (debug) {
41
+ (await import('node:inspector')).open(inspectorPort);
42
+ }
36
43
  const miniOxygen = await startServer({
37
44
  script: await readFile(buildPathWorkerFile),
38
45
  workerFile: buildPathWorkerFile,
@@ -90,10 +97,17 @@ async function startNodeServer({
90
97
  showBanner(options) {
91
98
  console.log("");
92
99
  renderSuccess({
93
- headline: `${options?.headlinePrefix ?? ""}MiniOxygen ${options?.mode ?? "development"} server running.`,
100
+ headline: `${options?.headlinePrefix ?? ""}MiniOxygen (Node Sandbox) ${options?.mode ?? "development"} server running.`,
94
101
  body: [
95
102
  `View ${options?.appName ?? "Hydrogen"} app: ${listeningAt}`,
96
- ...options?.extraLines ?? []
103
+ ...options?.extraLines ?? [],
104
+ ...debug ? [
105
+ {
106
+ warn: `
107
+
108
+ Debugger listening on ws://localhost:${inspectorPort}`
109
+ }
110
+ ] : []
97
111
  ]
98
112
  });
99
113
  console.log("");
@@ -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 (typeof lineNumber === "number") {
183
+ const pos = sourceMapConsumer.originalPositionFor({
184
+ line: lineNumber + 1,
185
+ column: columnNumber
186
+ });
187
+ if (i === 0 && pos.source && pos.line !== null) {
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 };