@shopify/cli-hydrogen 5.4.3 → 5.5.1

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 (38) hide show
  1. package/dist/commands/hydrogen/debug/cpu.js +98 -0
  2. package/dist/commands/hydrogen/dev.js +3 -4
  3. package/dist/commands/hydrogen/init.test.js +2 -2
  4. package/dist/generator-templates/starter/app/root.tsx +3 -1
  5. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +1 -1
  6. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +7 -2
  7. package/dist/generator-templates/starter/app/routes/account_.register.tsx +3 -3
  8. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +1 -0
  9. package/dist/generator-templates/starter/package.json +4 -3
  10. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +4 -4
  11. package/dist/hooks/init.js +4 -1
  12. package/dist/lib/cpu-profiler.js +92 -0
  13. package/dist/lib/mini-oxygen/node.js +5 -5
  14. package/dist/lib/mini-oxygen/workerd.js +21 -14
  15. package/dist/lib/onboarding/common.js +2 -2
  16. package/dist/lib/request-events.js +18 -7
  17. package/dist/lib/setups/i18n/domains.test.js +14 -0
  18. package/dist/lib/setups/i18n/index.js +3 -3
  19. package/dist/lib/setups/i18n/replacers.js +84 -64
  20. package/dist/lib/setups/i18n/replacers.test.js +242 -0
  21. package/dist/lib/setups/i18n/subdomains.test.js +14 -0
  22. package/dist/lib/setups/i18n/subfolders.test.js +14 -0
  23. package/dist/lib/setups/i18n/templates/domains.js +1 -1
  24. package/dist/lib/setups/i18n/templates/domains.ts +5 -2
  25. package/dist/lib/setups/i18n/templates/subdomains.ts +4 -1
  26. package/dist/lib/setups/i18n/templates/subfolders.js +1 -2
  27. package/dist/lib/setups/i18n/templates/subfolders.ts +7 -6
  28. package/dist/lib/setups/routes/generate.js +5 -2
  29. package/dist/lib/transpile/file.js +6 -0
  30. package/dist/lib/transpile/index.js +2 -0
  31. package/dist/lib/transpile/morph/classes.js +48 -0
  32. package/dist/lib/transpile/morph/functions.js +76 -0
  33. package/dist/lib/transpile/morph/index.js +67 -0
  34. package/dist/lib/transpile/morph/typedefs.js +133 -0
  35. package/dist/lib/transpile/morph/utils.js +15 -0
  36. package/dist/lib/{transpile-ts.js → transpile/project.js} +38 -59
  37. package/oclif.manifest.json +27 -1
  38. package/package.json +6 -6
@@ -0,0 +1,98 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { resolvePath, joinPath } from '@shopify/cli-kit/node/path';
3
+ import Command from '@shopify/cli-kit/node/base-command';
4
+ import { outputInfo, outputWarn } from '@shopify/cli-kit/node/output';
5
+ import colors from '@shopify/cli-kit/node/colors';
6
+ import { writeFile } from '@shopify/cli-kit/node/fs';
7
+ import ansiEscapes from 'ansi-escapes';
8
+ import { getProjectPaths, handleRemixImportFail, getRemixConfig } from '../../../lib/remix-config.js';
9
+ import { muteDevLogs, createRemixLogger } from '../../../lib/log.js';
10
+ import { commonFlags, flagsToCamelObject } from '../../../lib/flags.js';
11
+ import { createCpuStartupProfiler } from '../../../lib/cpu-profiler.js';
12
+
13
+ const DEFAULT_OUTPUT_PATH = "startup.cpuprofile";
14
+ class DebugCpu extends Command {
15
+ static description = "Builds and profiles the server startup time the app.";
16
+ static flags = {
17
+ path: commonFlags.path,
18
+ output: Flags.string({
19
+ description: `Specify a path to generate the profile file. Defaults to "${DEFAULT_OUTPUT_PATH}".`,
20
+ default: DEFAULT_OUTPUT_PATH,
21
+ required: false
22
+ })
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(DebugCpu);
26
+ const directory = flags.path ? resolvePath(flags.path) : process.cwd();
27
+ const output = flags.output ? resolvePath(flags.output) : joinPath(process.cwd(), flags.output);
28
+ await runDebugCpu({
29
+ ...flagsToCamelObject(flags),
30
+ path: directory,
31
+ output
32
+ });
33
+ }
34
+ }
35
+ async function runDebugCpu({
36
+ path: appPath,
37
+ output = DEFAULT_OUTPUT_PATH
38
+ }) {
39
+ if (!process.env.NODE_ENV)
40
+ process.env.NODE_ENV = "production";
41
+ muteDevLogs({ workerReload: false });
42
+ const { root, buildPathWorkerFile } = getProjectPaths(appPath);
43
+ outputInfo(
44
+ "\u23F3\uFE0F Starting profiler for CPU startup... Profile will be written to:\n" + colors.dim(output)
45
+ );
46
+ const runProfiler = await createCpuStartupProfiler();
47
+ const [{ watch }, { createFileWatchCache }] = await Promise.all([
48
+ import('@remix-run/dev/dist/compiler/watch.js'),
49
+ import('@remix-run/dev/dist/compiler/fileWatchCache.js')
50
+ ]).catch(handleRemixImportFail);
51
+ let times = 0;
52
+ const fileWatchCache = createFileWatchCache();
53
+ await watch(
54
+ {
55
+ config: await getRemixConfig(root),
56
+ options: {
57
+ mode: process.env.NODE_ENV,
58
+ sourcemap: true
59
+ },
60
+ fileWatchCache,
61
+ logger: createRemixLogger()
62
+ },
63
+ {
64
+ onBuildStart() {
65
+ if (times > 0) {
66
+ process.stdout.write(ansiEscapes.eraseLines(4));
67
+ }
68
+ outputInfo(`
69
+ #${++times} Building and profiling...`);
70
+ },
71
+ async onBuildFinish(context, duration, succeeded) {
72
+ if (succeeded) {
73
+ const { profile, totalScriptTimeMs } = await runProfiler(
74
+ buildPathWorkerFile
75
+ );
76
+ process.stdout.write(ansiEscapes.eraseLines(2));
77
+ outputInfo(
78
+ `#${times} Total time: ${totalScriptTimeMs.toLocaleString()} ms
79
+ ${colors.dim(output)}`
80
+ );
81
+ await writeFile(output, JSON.stringify(profile, null, 2));
82
+ outputInfo(`
83
+ Waiting for changes...`);
84
+ } else {
85
+ outputWarn("\nBuild failed, waiting for changes to restart...");
86
+ }
87
+ },
88
+ async onFileChanged(file) {
89
+ fileWatchCache.invalidateFile(file);
90
+ },
91
+ async onFileDeleted(file) {
92
+ fileWatchCache.invalidateFile(file);
93
+ }
94
+ }
95
+ );
96
+ }
97
+
98
+ export { DebugCpu as default };
@@ -122,7 +122,6 @@ async function runDev({
122
122
  },
123
123
  workerRuntime
124
124
  );
125
- const debugNetworkUrl = `${miniOxygen.listeningAt}/debug-network`;
126
125
  enhanceH2Logs({ host: miniOxygen.listeningAt, ...remixConfig });
127
126
  miniOxygen.showBanner({
128
127
  appName: storefront ? colors.cyan(storefront?.title) : void 0,
@@ -135,11 +134,11 @@ View GraphiQL API browser: ${getGraphiQLUrl({
135
134
  host: miniOxygen.listeningAt
136
135
  })}`
137
136
  ),
138
- workerRuntime ? "" : colors.dim(
137
+ colors.dim(
139
138
  `
140
- View server-side network requests: ${debugNetworkUrl}`
139
+ View server-side network requests: ${miniOxygen.listeningAt}/debug-network`
141
140
  )
142
- ].filter(Boolean)
141
+ ]
143
142
  });
144
143
  if (useCodegen) {
145
144
  spawnCodegenProcess({ ...remixConfig, configFilePath: codegenConfigPath });
@@ -171,7 +171,7 @@ describe("init", () => {
171
171
  )
172
172
  );
173
173
  await expect(readFile(`${tmpDir}/server.js`)).resolves.toMatch(
174
- /export default {\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/
174
+ /export default {\n\s+\/\*\*.*?\*\/\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/s
175
175
  );
176
176
  const output = outputMock.info();
177
177
  expect(output).toMatch("success");
@@ -266,7 +266,7 @@ describe("init", () => {
266
266
  );
267
267
  expect(projectFiles).toContain("app/routes/_index.jsx");
268
268
  await expect(readFile(`${tmpDir}/server.js`)).resolves.toMatch(
269
- /export default {\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/
269
+ /export default {\n\s+\/\*\*.*?\*\/\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/s
270
270
  );
271
271
  const output = outputMock.info();
272
272
  expect(output).toMatch("success");
@@ -21,7 +21,9 @@ import resetStyles from './styles/reset.css';
21
21
  import appStyles from './styles/app.css';
22
22
  import {Layout} from '~/components/Layout';
23
23
 
24
- // This is important to avoid re-fetching root queries on sub-navigations
24
+ /**
25
+ * This is important to avoid re-fetching root queries on sub-navigations
26
+ */
25
27
  export const shouldRevalidate: ShouldRevalidateFunction = ({
26
28
  formMethod,
27
29
  currentUrl,
@@ -164,8 +164,8 @@ export const CUSTOMER_FRAGMENT = `#graphql
164
164
  pageInfo {
165
165
  hasPreviousPage
166
166
  hasNextPage
167
- hasNextPage
168
167
  endCursor
168
+ startCursor
169
169
  }
170
170
  }
171
171
  }
@@ -1,4 +1,9 @@
1
- import {json, redirect, type LoaderArgs} from '@shopify/remix-oxygen';
1
+ import {
2
+ json,
3
+ redirect,
4
+ type LoaderArgs,
5
+ type ActionArgs,
6
+ } from '@shopify/remix-oxygen';
2
7
  import {Form, Link, useActionData} from '@remix-run/react';
3
8
 
4
9
  type ActionResponse = {
@@ -15,7 +20,7 @@ export async function loader({context}: LoaderArgs) {
15
20
  return json({});
16
21
  }
17
22
 
18
- export async function action({request, context}: LoaderArgs) {
23
+ export async function action({request, context}: ActionArgs) {
19
24
  const {storefront} = context;
20
25
  const form = await request.formData();
21
26
  const email = form.has('email') ? String(form.get('email')) : null;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  json,
3
3
  redirect,
4
- type ActionFunction,
5
4
  type LoaderArgs,
5
+ type ActionArgs,
6
6
  } from '@shopify/remix-oxygen';
7
7
  import {Form, Link, useActionData} from '@remix-run/react';
8
8
  import type {CustomerCreateMutation} from 'storefrontapi.generated';
@@ -23,7 +23,7 @@ export async function loader({context}: LoaderArgs) {
23
23
  return json({});
24
24
  }
25
25
 
26
- export const action: ActionFunction = async ({request, context}) => {
26
+ export async function action({request, context}: ActionArgs) {
27
27
  if (request.method !== 'POST') {
28
28
  return json({error: 'Method not allowed'}, {status: 405});
29
29
  }
@@ -101,7 +101,7 @@ export const action: ActionFunction = async ({request, context}) => {
101
101
  }
102
102
  return json({error}, {status: 400});
103
103
  }
104
- };
104
+ }
105
105
 
106
106
  export default function Register() {
107
107
  const data = useActionData<ActionResponse>();
@@ -133,6 +133,7 @@ const BLOGS_QUERY = `#graphql
133
133
  hasNextPage
134
134
  hasNextPage
135
135
  endCursor
136
+ startCursor
136
137
  }
137
138
 
138
139
  }
@@ -15,9 +15,9 @@
15
15
  "dependencies": {
16
16
  "@remix-run/react": "1.19.1",
17
17
  "@shopify/cli": "3.49.2",
18
- "@shopify/cli-hydrogen": "^5.4.3",
19
- "@shopify/hydrogen": "^2023.7.11",
20
- "@shopify/remix-oxygen": "^1.1.7",
18
+ "@shopify/cli-hydrogen": "^5.5.1",
19
+ "@shopify/hydrogen": "^2023.7.13",
20
+ "@shopify/remix-oxygen": "^1.1.8",
21
21
  "graphql": "^16.6.0",
22
22
  "graphql-tag": "^2.12.6",
23
23
  "isbot": "^3.6.6",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@remix-run/dev": "1.19.1",
29
+ "@remix-run/eslint-config": "1.19.1",
29
30
  "@shopify/oxygen-workers-types": "^3.17.3",
30
31
  "@shopify/prettier-config": "^1.1.2",
31
32
  "@total-typescript/ts-reset": "^0.4.2",
@@ -637,7 +637,7 @@ export type CustomerOrdersFragment = Pick<
637
637
  >;
638
638
  pageInfo: Pick<
639
639
  StorefrontAPI.PageInfo,
640
- 'hasPreviousPage' | 'hasNextPage' | 'endCursor'
640
+ 'hasPreviousPage' | 'hasNextPage' | 'endCursor' | 'startCursor'
641
641
  >;
642
642
  };
643
643
  };
@@ -689,7 +689,7 @@ export type CustomerOrdersQuery = {
689
689
  >;
690
690
  pageInfo: Pick<
691
691
  StorefrontAPI.PageInfo,
692
- 'hasPreviousPage' | 'hasNextPage' | 'endCursor'
692
+ 'hasPreviousPage' | 'hasNextPage' | 'endCursor' | 'startCursor'
693
693
  >;
694
694
  };
695
695
  }
@@ -1113,7 +1113,7 @@ export type BlogQuery = {
1113
1113
  >;
1114
1114
  pageInfo: Pick<
1115
1115
  StorefrontAPI.PageInfo,
1116
- 'hasPreviousPage' | 'hasNextPage' | 'endCursor'
1116
+ 'hasPreviousPage' | 'hasNextPage' | 'endCursor' | 'startCursor'
1117
1117
  >;
1118
1118
  };
1119
1119
  }
@@ -1811,7 +1811,7 @@ interface GeneratedQueryTypes {
1811
1811
  return: ArticleQuery;
1812
1812
  variables: ArticleQueryVariables;
1813
1813
  };
1814
- '#graphql\n query Blog(\n $language: LanguageCode\n $blogHandle: String!\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(language: $language) {\n blog(handle: $blogHandle) {\n title\n seo {\n title\n description\n }\n articles(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ArticleItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n hasNextPage\n endCursor\n }\n\n }\n }\n }\n fragment ArticleItem on Article {\n author: authorV2 {\n name\n }\n contentHtml\n handle\n id\n image {\n id\n altText\n url\n width\n height\n }\n publishedAt\n title\n blog {\n handle\n }\n }\n': {
1814
+ '#graphql\n query Blog(\n $language: LanguageCode\n $blogHandle: String!\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(language: $language) {\n blog(handle: $blogHandle) {\n title\n seo {\n title\n description\n }\n articles(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ArticleItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n hasNextPage\n endCursor\n startCursor\n }\n\n }\n }\n }\n fragment ArticleItem on Article {\n author: authorV2 {\n name\n }\n contentHtml\n handle\n id\n image {\n id\n altText\n url\n width\n height\n }\n publishedAt\n title\n blog {\n handle\n }\n }\n': {
1815
1815
  return: BlogQuery;
1816
1816
  variables: BlogQueryVariables;
1817
1817
  };
@@ -3,7 +3,10 @@ import { outputDebug } from '@shopify/cli-kit/node/output';
3
3
 
4
4
  const EXPERIMENTAL_VM_MODULES_FLAG = "--experimental-vm-modules";
5
5
  const hook = async function(options) {
6
- if (options.id && ["hydrogen:dev", "hydrogen:preview"].includes(options.id) && !process.execArgv.includes(EXPERIMENTAL_VM_MODULES_FLAG) && !(process.env.NODE_OPTIONS ?? "").includes(EXPERIMENTAL_VM_MODULES_FLAG)) {
6
+ if (options.id && // All the commands that rely on MiniOxygen:
7
+ ["hydrogen:dev", "hydrogen:preview", "hydrogen:debug:cpu"].includes(
8
+ options.id
9
+ ) && !process.execArgv.includes(EXPERIMENTAL_VM_MODULES_FLAG) && !(process.env.NODE_OPTIONS ?? "").includes(EXPERIMENTAL_VM_MODULES_FLAG)) {
7
10
  outputDebug(
8
11
  `Restarting CLI process with ${EXPERIMENTAL_VM_MODULES_FLAG} flag.`
9
12
  );
@@ -0,0 +1,92 @@
1
+ import { readFile } from '@shopify/cli-kit/node/fs';
2
+ import { Session } from 'node:inspector';
3
+
4
+ async function createCpuStartupProfiler() {
5
+ const { createMiniOxygen } = await import('@shopify/mini-oxygen');
6
+ const miniOxygen = createMiniOxygen({
7
+ script: "export default {}",
8
+ modules: true,
9
+ log: () => {
10
+ }
11
+ });
12
+ await miniOxygen.ready();
13
+ return async (scriptPath) => {
14
+ const script = await readFile(scriptPath);
15
+ const stopProfiler = await startProfiler();
16
+ await miniOxygen.reload({ script });
17
+ const rawProfile = await stopProfiler();
18
+ return enhanceProfileNodes(rawProfile, scriptPath + ".map");
19
+ };
20
+ }
21
+ function startProfiler() {
22
+ const session = new Session();
23
+ session.connect();
24
+ return new Promise((resolveStart) => {
25
+ session.post("Profiler.enable", () => {
26
+ session.post("Profiler.start", () => {
27
+ resolveStart(() => {
28
+ return new Promise((resolveStop, rejectStop) => {
29
+ session.post("Profiler.stop", (err, { profile }) => {
30
+ session.disconnect();
31
+ if (err) {
32
+ return rejectStop(err);
33
+ }
34
+ resolveStop(profile);
35
+ });
36
+ });
37
+ });
38
+ });
39
+ });
40
+ });
41
+ }
42
+ async function enhanceProfileNodes(profile, sourceMapPath) {
43
+ const { SourceMapConsumer } = await import('source-map');
44
+ const sourceMap = JSON.parse(await readFile(sourceMapPath));
45
+ const smc = await new SourceMapConsumer(sourceMap, "file://" + sourceMapPath);
46
+ const scriptDescendants = /* @__PURE__ */ new Set();
47
+ let totalScriptTimeMicrosec = 0;
48
+ const totalTimeMicrosec = profile.endTime - profile.startTime;
49
+ const timePerSample = profile.samples?.length ? totalTimeMicrosec / profile.samples.length : 0;
50
+ for (const node of profile.nodes) {
51
+ if (node.callFrame.url === "<script>" || scriptDescendants.has(node.id)) {
52
+ scriptDescendants.add(node.id);
53
+ node.children?.forEach((id) => scriptDescendants.add(id));
54
+ }
55
+ if (scriptDescendants.has(node.id)) {
56
+ augmentNode(node, smc);
57
+ totalScriptTimeMicrosec += Math.round((node.hitCount ?? 0) * timePerSample * 1e3) / 1e3;
58
+ } else {
59
+ silenceNode(node);
60
+ }
61
+ }
62
+ smc.destroy();
63
+ return {
64
+ profile,
65
+ totalTimeMs: totalTimeMicrosec / 1e3,
66
+ totalScriptTimeMs: totalScriptTimeMicrosec / 1e3
67
+ };
68
+ }
69
+ function augmentNode(node, smc) {
70
+ const originalPosition = smc.originalPositionFor({
71
+ line: node.callFrame.lineNumber + 1,
72
+ column: node.callFrame.columnNumber + 1
73
+ });
74
+ node.callFrame.url = originalPosition.source || node.callFrame.url;
75
+ node.callFrame.functionName = originalPosition.name || node.callFrame.functionName;
76
+ node.callFrame.lineNumber = originalPosition.line ? originalPosition.line - 1 : node.callFrame.lineNumber;
77
+ node.callFrame.columnNumber = originalPosition.column ?? node.callFrame.columnNumber;
78
+ }
79
+ function silenceNode(node) {
80
+ Object.assign(node, {
81
+ children: [],
82
+ callFrame: {
83
+ functionName: "(profiler)",
84
+ scriptId: "0",
85
+ url: "",
86
+ lineNumber: -1,
87
+ columnNumber: -1
88
+ }
89
+ });
90
+ }
91
+
92
+ export { createCpuStartupProfiler };
@@ -5,7 +5,7 @@ import { renderSuccess } from '@shopify/cli-kit/node/ui';
5
5
  import { 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 { clearHistory, streamRequestEvents, logRequestEvent } from '../request-events.js';
8
+ import { handleDebugNetworkRequest, H2O_BINDING_NAME, logRequestEvent } from '../request-events.js';
9
9
 
10
10
  async function startNodeServer({
11
11
  port = DEFAULT_PORT,
@@ -21,13 +21,13 @@ async function startNodeServer({
21
21
  );
22
22
  const asyncLocalStorage = new AsyncLocalStorage();
23
23
  const serviceBindings = {
24
- H2O_LOG_EVENT: {
24
+ [H2O_BINDING_NAME]: {
25
25
  fetch: async (request) => logRequestEvent(
26
26
  new Request(request.url, {
27
27
  method: "POST",
28
28
  body: JSON.stringify({
29
- ...await request.json(),
30
- ...asyncLocalStorage.getStore()
29
+ ...asyncLocalStorage.getStore(),
30
+ ...await request.json()
31
31
  })
32
32
  })
33
33
  )
@@ -53,7 +53,7 @@ async function startNodeServer({
53
53
  async onRequest(request, defaultDispatcher) {
54
54
  const url = new URL(request.url);
55
55
  if (url.pathname === "/debug-network-server") {
56
- return request.method === "DELETE" ? clearHistory() : streamRequestEvents(request);
56
+ return handleDebugNetworkRequest(request);
57
57
  }
58
58
  let requestId = request.headers.get("request-id");
59
59
  if (!requestId) {
@@ -1,4 +1,4 @@
1
- import { Miniflare, NoOpLog, Response, Request } from 'miniflare';
1
+ import { Response, Miniflare, NoOpLog, Request } from 'miniflare';
2
2
  import { resolvePath } from '@shopify/cli-kit/node/path';
3
3
  import { glob, readFile, createFileReadStream, fileSize } from '@shopify/cli-kit/node/fs';
4
4
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
@@ -7,7 +7,9 @@ import { findInspectorUrl, connectToInspector } from './workerd-inspector.js';
7
7
  import { DEFAULT_PORT } from '../flags.js';
8
8
  import { findPort } from '../find-port.js';
9
9
  import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
10
+ import { setConstructors, handleDebugNetworkRequest, H2O_BINDING_NAME, logRequestEvent } from '../request-events.js';
10
11
 
12
+ const DEFAULT_INSPECTOR_PORT = 8787;
11
13
  async function startWorkerdServer({
12
14
  root,
13
15
  port = DEFAULT_PORT,
@@ -16,7 +18,8 @@ async function startWorkerdServer({
16
18
  buildPathClient,
17
19
  env
18
20
  }) {
19
- const inspectorPort = await findPort(8787);
21
+ const appPort = await findPort(port);
22
+ const inspectorPort = await findPort(DEFAULT_INSPECTOR_PORT);
20
23
  const oxygenHeadersMap = Object.values(OXYGEN_HEADERS_MAP).reduce(
21
24
  (acc, item) => {
22
25
  acc[item.name] = item.defaultValue;
@@ -24,13 +27,14 @@ async function startWorkerdServer({
24
27
  },
25
28
  {}
26
29
  );
30
+ setConstructors({ Response });
27
31
  const buildMiniOxygenOptions = async () => ({
28
32
  cf: false,
29
33
  verbose: false,
30
- port,
34
+ port: appPort,
35
+ inspectorPort,
31
36
  log: new NoOpLog(),
32
37
  liveReload: watch,
33
- inspectorPort,
34
38
  host: "localhost",
35
39
  workers: [
36
40
  {
@@ -44,6 +48,7 @@ async function startWorkerdServer({
44
48
  serviceBindings: {
45
49
  hydrogen: "hydrogen",
46
50
  assets: createAssetHandler(buildPathClient),
51
+ debugNetwork: handleDebugNetworkRequest,
47
52
  logRequest
48
53
  }
49
54
  },
@@ -56,9 +61,12 @@ async function startWorkerdServer({
56
61
  contents: await readFile(resolvePath(root, buildPathWorkerFile))
57
62
  }
58
63
  ],
59
- bindings: { ...env },
60
64
  compatibilityFlags: ["streams_enable_constructors"],
61
- compatibilityDate: "2022-10-31"
65
+ compatibilityDate: "2022-10-31",
66
+ bindings: { ...env },
67
+ serviceBindings: {
68
+ [H2O_BINDING_NAME]: logRequestEvent
69
+ }
62
70
  }
63
71
  ]
64
72
  });
@@ -69,7 +77,7 @@ async function startWorkerdServer({
69
77
  let inspectorUrl = await findInspectorUrl(inspectorPort);
70
78
  let cleanupInspector = inspectorUrl ? connectToInspector({ inspectorUrl, sourceMapPath }) : void 0;
71
79
  return {
72
- port,
80
+ port: appPort,
73
81
  listeningAt,
74
82
  async reload(nextOptions) {
75
83
  miniOxygenOptions = await buildMiniOxygenOptions();
@@ -105,13 +113,11 @@ async function startWorkerdServer({
105
113
  };
106
114
  }
107
115
  async function miniOxygenHandler(request, env, context) {
116
+ const { pathname } = new URL(request.url);
117
+ if (pathname === "/debug-network-server") {
118
+ return env.debugNetwork.fetch(request);
119
+ }
108
120
  if (request.method === "GET") {
109
- const pathname = new URL(request.url).pathname;
110
- if (pathname.startsWith("/debug-network")) {
111
- return new Response(
112
- "The Network Debugger is currently not supported in the Worker Runtime."
113
- );
114
- }
115
121
  if (new Set(env.initialAssets).has(pathname.slice(1))) {
116
122
  const response2 = await env.assets.fetch(
117
123
  new Request(request.url, {
@@ -125,6 +131,7 @@ async function miniOxygenHandler(request, env, context) {
125
131
  }
126
132
  const requestInit = {
127
133
  headers: {
134
+ "request-id": crypto.randomUUID(),
128
135
  ...env.oxygenHeadersMap,
129
136
  ...Object.fromEntries(request.headers.entries())
130
137
  }
@@ -138,7 +145,7 @@ async function miniOxygenHandler(request, env, context) {
138
145
  method: request.method,
139
146
  signal: request.signal,
140
147
  headers: {
141
- ...Object.fromEntries(request.headers.entries()),
148
+ ...requestInit.headers,
142
149
  "h2-duration-ms": String(durationMs),
143
150
  "h2-response-status": String(response.status)
144
151
  }
@@ -12,7 +12,7 @@ import { login, renderLoginSuccess } from '../auth.js';
12
12
  import { renderI18nPrompt, setupI18nStrategy, I18N_STRATEGY_NAME_MAP } from '../setups/i18n/index.js';
13
13
  import { titleize } from '../string.js';
14
14
  import { ALIAS_NAME, createPlatformShortcut, getCliCommand } from '../shell.js';
15
- import { transpileProject } from '../transpile-ts.js';
15
+ import { transpileProject } from '../transpile/index.js';
16
16
  import { renderCssPrompt, setupCssStrategy, CSS_STRATEGY_NAME_MAP } from '../setups/css/index.js';
17
17
  import { renderRoutePrompt, generateRoutes, generateProjectFile } from '../setups/routes/generate.js';
18
18
  import { execAsync } from '../process.js';
@@ -193,7 +193,7 @@ async function handleLanguage(projectDir, controller, flagLanguage) {
193
193
  return {
194
194
  language,
195
195
  async transpileProject() {
196
- if (language === "js") {
196
+ if (language !== "ts") {
197
197
  await transpileProject(projectDir);
198
198
  }
199
199
  }
@@ -1,8 +1,13 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { ReadableStream } from 'node:stream/web';
3
- import { Response } from '@shopify/mini-oxygen';
4
3
  import { getGraphiQLUrl } from './graphiql-url.js';
4
+ import { Response } from '@shopify/mini-oxygen';
5
5
 
6
+ const H2O_BINDING_NAME = "H2O_LOG_EVENT";
7
+ let ResponseConstructor = Response;
8
+ function setConstructors(constructors) {
9
+ ResponseConstructor = constructors.Response;
10
+ }
6
11
  const DEV_ROUTES = /* @__PURE__ */ new Set(["/graphiql", "/debug-network"]);
7
12
  const EVENT_MAP = {
8
13
  request: "Request",
@@ -22,14 +27,17 @@ async function getRequestInfo(request) {
22
27
  }
23
28
  const eventEmitter = new EventEmitter();
24
29
  const eventHistory = [];
25
- async function clearHistory() {
30
+ function createResponse(main = "ok", init) {
31
+ return new ResponseConstructor(main, init);
32
+ }
33
+ async function clearHistory(request) {
26
34
  eventHistory.length = 0;
27
- return new Response("ok");
35
+ return createResponse();
28
36
  }
29
37
  async function logRequestEvent(request) {
30
38
  const url = new URL(request.url);
31
39
  if (DEV_ROUTES.has(url.pathname)) {
32
- return new Response("ok");
40
+ return createResponse();
33
41
  }
34
42
  const { eventType, purpose, graphql, ...data } = await getRequestInfo(request);
35
43
  let graphiqlLink = "";
@@ -52,7 +60,7 @@ async function logRequestEvent(request) {
52
60
  if (eventHistory.length > 100)
53
61
  eventHistory.shift();
54
62
  eventEmitter.emit("request", event);
55
- return new Response("ok");
63
+ return createResponse();
56
64
  }
57
65
  function streamRequestEvents(request) {
58
66
  const stream = new ReadableStream({
@@ -81,7 +89,7 @@ function streamRequestEvents(request) {
81
89
  return close();
82
90
  }
83
91
  });
84
- return new Response(stream, {
92
+ return createResponse(stream, {
85
93
  headers: {
86
94
  "Content-Type": "text/event-stream",
87
95
  "Cache-Control": "no-store",
@@ -89,5 +97,8 @@ function streamRequestEvents(request) {
89
97
  }
90
98
  });
91
99
  }
100
+ function handleDebugNetworkRequest(request) {
101
+ return request.method === "DELETE" ? clearHistory() : streamRequestEvents(request);
102
+ }
92
103
 
93
- export { DEV_ROUTES, clearHistory, logRequestEvent, streamRequestEvents };
104
+ export { DEV_ROUTES, H2O_BINDING_NAME, handleDebugNetworkRequest, logRequestEvent, setConstructors };
@@ -1,5 +1,7 @@
1
+ import { fileURLToPath } from 'node:url';
1
2
  import { describe, it, expect } from 'vitest';
2
3
  import { getLocaleFromRequest } from './templates/domains.js';
4
+ import { readFile } from '@shopify/cli-kit/node/fs';
3
5
 
4
6
  describe("Setup i18n with domains", () => {
5
7
  it("extracts the locale from the domain", () => {
@@ -22,4 +24,16 @@ describe("Setup i18n with domains", () => {
22
24
  country: "ES"
23
25
  });
24
26
  });
27
+ it("does not access imported types directly", async () => {
28
+ const template = await readFile(
29
+ fileURLToPath(new URL("./templates/domains.ts", import.meta.url))
30
+ );
31
+ const typeImports = (template.match(/import\s+type\s+{([^}]+)}/)?.[1] || "").trim().split(/\s*,\s*/);
32
+ expect(typeImports).not.toHaveLength(0);
33
+ const fnCode = template.match(/function .*\n}$/ms)?.[0] || "";
34
+ expect(fnCode).toBeTruthy();
35
+ typeImports.forEach(
36
+ (typeImport) => expect(fnCode).not.toContain(typeImport)
37
+ );
38
+ });
25
39
  });
@@ -16,16 +16,16 @@ const I18N_STRATEGY_NAME_MAP = {
16
16
  };
17
17
  const I18N_CHOICES = [...SETUP_I18N_STRATEGIES, "none"];
18
18
  async function setupI18nStrategy(strategy, options) {
19
- const isTs = options.serverEntryPoint?.endsWith(".ts") ?? false;
19
+ const isJs = options.serverEntryPoint?.endsWith(".js") ?? false;
20
20
  const templatePath = fileURLToPath(
21
- new URL(`./templates/${strategy}${isTs ? ".ts" : ".js"}`, import.meta.url)
21
+ new URL(`./templates/${strategy}.ts`, import.meta.url)
22
22
  );
23
23
  if (!await fileExists(templatePath)) {
24
24
  throw new Error("Unknown strategy");
25
25
  }
26
26
  const template = await readFile(templatePath);
27
27
  const formatConfig = await getCodeFormatOptions(options.rootDirectory);
28
- await replaceServerI18n(options, formatConfig, template);
28
+ await replaceServerI18n(options, formatConfig, template, isJs);
29
29
  await replaceRemixEnv(options, formatConfig, template);
30
30
  }
31
31
  async function renderI18nPrompt(options) {