@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.
- package/dist/commands/hydrogen/build.js +40 -78
- package/dist/commands/hydrogen/codegen.js +8 -3
- package/dist/commands/hydrogen/deploy.js +173 -37
- package/dist/commands/hydrogen/deploy.test.js +192 -20
- package/dist/commands/hydrogen/dev.js +56 -31
- package/dist/commands/hydrogen/init.js +1 -1
- package/dist/commands/hydrogen/init.test.js +155 -53
- package/dist/commands/hydrogen/link.js +5 -21
- package/dist/commands/hydrogen/link.test.js +10 -10
- package/dist/commands/hydrogen/preview.js +22 -11
- package/dist/commands/hydrogen/setup.js +0 -4
- package/dist/commands/hydrogen/setup.test.js +0 -1
- package/dist/commands/hydrogen/shortcut.js +1 -0
- package/dist/commands/hydrogen/upgrade.js +720 -0
- package/dist/commands/hydrogen/upgrade.test.js +786 -0
- package/dist/generator-templates/starter/.graphqlrc.yml +12 -1
- package/dist/generator-templates/starter/CHANGELOG.md +126 -0
- package/dist/generator-templates/starter/README.md +23 -0
- package/dist/generator-templates/starter/app/components/Cart.tsx +1 -1
- package/dist/generator-templates/starter/app/components/Footer.tsx +3 -1
- package/dist/generator-templates/starter/app/components/Header.tsx +5 -1
- package/dist/generator-templates/starter/app/components/Layout.tsx +14 -11
- package/dist/generator-templates/starter/app/components/Search.tsx +1 -1
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +39 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
- package/dist/generator-templates/starter/app/lib/fragments.ts +102 -0
- package/dist/generator-templates/starter/app/lib/session.ts +67 -0
- package/dist/generator-templates/starter/app/root.tsx +11 -45
- package/dist/generator-templates/starter/app/routes/[robots.txt].tsx +0 -27
- package/dist/generator-templates/starter/app/routes/account.$.tsx +8 -4
- package/dist/generator-templates/starter/app/routes/account._index.tsx +5 -0
- package/dist/generator-templates/starter/app/routes/account.addresses.tsx +215 -206
- package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +56 -163
- package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +32 -109
- package/dist/generator-templates/starter/app/routes/account.profile.tsx +40 -180
- package/dist/generator-templates/starter/app/routes/account.tsx +20 -135
- package/dist/generator-templates/starter/app/routes/account_.authorize.tsx +5 -0
- package/dist/generator-templates/starter/app/routes/account_.login.tsx +3 -140
- package/dist/generator-templates/starter/app/routes/account_.logout.tsx +5 -24
- package/dist/generator-templates/starter/app/routes/cart.tsx +7 -5
- package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/products.$handle.tsx +2 -2
- package/dist/generator-templates/starter/app/routes/search.tsx +1 -1
- package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +506 -0
- package/dist/generator-templates/starter/package.json +11 -10
- package/dist/generator-templates/starter/remix.config.js +4 -0
- package/dist/generator-templates/starter/remix.env.d.ts +6 -11
- package/dist/generator-templates/starter/server.ts +24 -167
- package/dist/generator-templates/starter/storefrontapi.generated.d.ts +104 -881
- package/dist/hooks/init.js +4 -4
- package/dist/lib/auth.js +5 -10
- package/dist/lib/build.js +6 -1
- package/dist/lib/bundle/analyzer.js +36 -26
- package/dist/lib/check-lockfile.js +1 -0
- package/dist/lib/codegen.js +59 -18
- package/dist/lib/defer.js +12 -0
- package/dist/lib/file.js +52 -3
- package/dist/lib/flags.js +27 -9
- package/dist/lib/get-oxygen-deployment-data.test.js +4 -2
- package/dist/lib/graphql/admin/client.test.js +2 -2
- package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
- package/dist/lib/log.js +32 -14
- package/dist/lib/mini-oxygen/assets.js +118 -0
- package/dist/lib/mini-oxygen/common.js +2 -1
- package/dist/lib/mini-oxygen/index.js +7 -5
- package/dist/lib/mini-oxygen/mini-oxygen.test.js +214 -0
- package/dist/lib/mini-oxygen/node.js +19 -5
- package/dist/lib/mini-oxygen/workerd-inspector-logs.js +227 -0
- package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +200 -0
- package/dist/lib/mini-oxygen/workerd-inspector.js +62 -235
- package/dist/lib/mini-oxygen/workerd.js +74 -50
- package/dist/lib/missing-routes.js +6 -3
- package/dist/lib/onboarding/common.js +40 -9
- package/dist/lib/onboarding/local.js +19 -11
- package/dist/lib/onboarding/remote.js +48 -28
- package/dist/lib/render-errors.js +2 -0
- package/dist/lib/request-events.js +65 -31
- package/dist/lib/setups/css/assets.js +1 -46
- package/dist/lib/setups/css/css-modules.js +3 -2
- package/dist/lib/setups/css/postcss.js +4 -2
- package/dist/lib/setups/css/tailwind.js +4 -2
- package/dist/lib/setups/css/vanilla-extract.js +3 -2
- package/dist/lib/setups/i18n/replacers.test.js +56 -38
- package/dist/lib/shell.js +1 -1
- package/dist/lib/template-diff.js +89 -0
- package/dist/lib/template-downloader.js +3 -2
- package/dist/lib/transpile/project.js +1 -1
- package/dist/virtual-routes/assets/debug-network.css +592 -0
- package/dist/virtual-routes/assets/favicon-dark.svg +20 -0
- package/dist/virtual-routes/components/FlameChartWrapper.jsx +8 -10
- package/dist/virtual-routes/components/IconClose.jsx +38 -0
- package/dist/virtual-routes/components/IconDiscard.jsx +44 -0
- package/dist/virtual-routes/components/RequestDetails.jsx +179 -0
- package/dist/virtual-routes/components/RequestTable.jsx +92 -0
- package/dist/virtual-routes/components/RequestWaterfall.jsx +151 -0
- package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +176 -0
- package/dist/virtual-routes/routes/subrequest-profiler.jsx +243 -0
- package/oclif.manifest.json +134 -59
- package/package.json +18 -26
- package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +0 -161
- package/dist/generator-templates/starter/app/routes/account_.recover.tsx +0 -129
- package/dist/generator-templates/starter/app/routes/account_.register.tsx +0 -207
- package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +0 -136
- package/dist/virtual-routes/routes/debug-network.jsx +0 -289
- /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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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 };
|