@shopify/cli-hydrogen 5.2.3 → 5.3.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 (37) hide show
  1. package/dist/commands/hydrogen/build.js +42 -20
  2. package/dist/commands/hydrogen/deploy.js +171 -0
  3. package/dist/commands/hydrogen/deploy.test.js +185 -0
  4. package/dist/commands/hydrogen/dev.js +21 -13
  5. package/dist/commands/hydrogen/init.js +10 -6
  6. package/dist/commands/hydrogen/init.test.js +16 -1
  7. package/dist/commands/hydrogen/preview.js +27 -11
  8. package/dist/generator-templates/starter/app/root.tsx +6 -4
  9. package/dist/generator-templates/starter/app/routes/account.tsx +1 -1
  10. package/dist/generator-templates/starter/app/routes/cart.$lines.tsx +70 -0
  11. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -1
  12. package/dist/generator-templates/starter/app/routes/discount.$code.tsx +43 -0
  13. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +3 -1
  14. package/dist/generator-templates/starter/package.json +3 -3
  15. package/dist/generator-templates/starter/remix.env.d.ts +11 -3
  16. package/dist/generator-templates/starter/server.ts +21 -18
  17. package/dist/generator-templates/starter/tsconfig.json +1 -1
  18. package/dist/lib/bundle/analyzer.js +56 -0
  19. package/dist/lib/bundle/bundle-analyzer.html +2045 -0
  20. package/dist/lib/flags.js +4 -0
  21. package/dist/lib/get-oxygen-token.js +47 -0
  22. package/dist/lib/get-oxygen-token.test.js +104 -0
  23. package/dist/lib/graphql/admin/oxygen-token.js +21 -0
  24. package/dist/lib/log.js +56 -13
  25. package/dist/lib/mini-oxygen/common.js +58 -0
  26. package/dist/lib/mini-oxygen/index.js +12 -0
  27. package/dist/lib/{mini-oxygen.js → mini-oxygen/node.js} +27 -52
  28. package/dist/lib/mini-oxygen/types.js +1 -0
  29. package/dist/lib/mini-oxygen/workerd-inspector.js +392 -0
  30. package/dist/lib/mini-oxygen/workerd.js +182 -0
  31. package/dist/lib/onboarding/common.js +4 -4
  32. package/dist/lib/onboarding/local.js +1 -1
  33. package/dist/lib/render-errors.js +1 -1
  34. package/dist/lib/setups/routes/generate.js +1 -1
  35. package/dist/virtual-routes/routes/index.jsx +4 -4
  36. package/oclif.manifest.json +81 -3
  37. package/package.json +12 -4
package/dist/lib/flags.js CHANGED
@@ -17,6 +17,10 @@ const commonFlags = {
17
17
  env: "SHOPIFY_HYDROGEN_FLAG_PORT",
18
18
  default: DEFAULT_PORT
19
19
  }),
20
+ workerRuntime: Flags.boolean({
21
+ description: "Run the app in a worker environment closer to Oxygen production instead of a Node.js sandbox. This flag is unstable and may change without notice.",
22
+ env: "SHOPIFY_HYDROGEN_FLAG_WORKER_UNSTABLE"
23
+ }),
20
24
  force: Flags.boolean({
21
25
  description: "Overwrite the destination directory and files if they already exist.",
22
26
  env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
@@ -0,0 +1,47 @@
1
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
2
+ import { outputWarn } from '@shopify/cli-kit/node/output';
3
+ import { linkStorefront } from '../commands/hydrogen/link.js';
4
+ import { login } from './auth.js';
5
+ import { getCliCommand } from './shell.js';
6
+ import { renderMissingLink, renderMissingStorefront } from './render-errors.js';
7
+ import { getOxygenToken } from './graphql/admin/oxygen-token.js';
8
+
9
+ async function getOxygenDeploymentToken({
10
+ root
11
+ }) {
12
+ const [{ session, config }, cliCommand] = await Promise.all([
13
+ login(root),
14
+ getCliCommand()
15
+ ]);
16
+ if (!config.storefront?.id) {
17
+ renderMissingLink({ session, cliCommand });
18
+ const runLink = await renderConfirmationPrompt({
19
+ message: ["Run", { command: `${cliCommand} link` }]
20
+ });
21
+ if (!runLink) {
22
+ return;
23
+ }
24
+ config.storefront = await linkStorefront(root, session, config, {
25
+ cliCommand
26
+ });
27
+ }
28
+ if (!config.storefront) {
29
+ return;
30
+ }
31
+ const { storefront } = await getOxygenToken(session, config.storefront.id);
32
+ if (!storefront) {
33
+ renderMissingStorefront({
34
+ session,
35
+ storefront: config.storefront,
36
+ cliCommand
37
+ });
38
+ return;
39
+ }
40
+ if (!storefront.oxygenDeploymentToken) {
41
+ outputWarn(`Could not retrieve a deployment token.`);
42
+ return;
43
+ }
44
+ return storefront.oxygenDeploymentToken;
45
+ }
46
+
47
+ export { getOxygenDeploymentToken };
@@ -0,0 +1,104 @@
1
+ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
3
+ import { getOxygenDeploymentToken } from './get-oxygen-token.js';
4
+ import { login } from './auth.js';
5
+ import { getConfig } from './shopify-config.js';
6
+ import { renderMissingLink, renderMissingStorefront } from './render-errors.js';
7
+ import { linkStorefront } from '../commands/hydrogen/link.js';
8
+ import { getOxygenToken } from './graphql/admin/oxygen-token.js';
9
+
10
+ vi.mock("@shopify/cli-kit/node/ui", async () => {
11
+ const original = await vi.importActual("@shopify/cli-kit/node/ui");
12
+ return {
13
+ ...original,
14
+ renderConfirmationPrompt: vi.fn()
15
+ };
16
+ });
17
+ vi.mock("./auth.js");
18
+ vi.mock("./admin-session.js");
19
+ vi.mock("./shopify-config.js");
20
+ vi.mock("./render-errors.js");
21
+ vi.mock("../commands/hydrogen/link.js");
22
+ vi.mock("./graphql/admin/oxygen-token.js");
23
+ describe("getOxygenDeploymentToken", () => {
24
+ const OXYGEN_DEPLOYMENT_TOKEN = "a-lovely-token";
25
+ beforeEach(() => {
26
+ vi.mocked(login).mockResolvedValue({
27
+ session: {
28
+ token: "123",
29
+ storeFqdn: "www.snowdevil.com"
30
+ },
31
+ config: {
32
+ shop: "snowdevil.myshopify.com",
33
+ shopName: "Snowdevil",
34
+ email: "merchant@shop.com",
35
+ storefront: {
36
+ id: "1",
37
+ title: "Snowboards"
38
+ }
39
+ }
40
+ });
41
+ vi.mocked(getConfig).mockResolvedValue({
42
+ storefront: { id: "storefront-id", title: "Existing Link" }
43
+ });
44
+ vi.mocked(getOxygenToken).mockResolvedValue({
45
+ storefront: { oxygenDeploymentToken: OXYGEN_DEPLOYMENT_TOKEN }
46
+ });
47
+ });
48
+ afterEach(() => {
49
+ vi.resetAllMocks();
50
+ });
51
+ it("returns the oxygen deployment token", async () => {
52
+ const token = await getOxygenDeploymentToken({ root: "test-root" });
53
+ expect(token).toBe(OXYGEN_DEPLOYMENT_TOKEN);
54
+ });
55
+ describe("when there is no linked storefront", () => {
56
+ beforeEach(() => {
57
+ vi.mocked(login).mockResolvedValue({
58
+ session: {
59
+ token: "123",
60
+ storeFqdn: "www.snowdevil.com"
61
+ },
62
+ config: {
63
+ shop: "snowdevil.myshopify.com",
64
+ shopName: "Snowdevil",
65
+ email: "merchant@shop.com",
66
+ storefront: void 0
67
+ }
68
+ });
69
+ });
70
+ it("calls renderMissingLink and prompts the user to create a link", async () => {
71
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
72
+ await getOxygenDeploymentToken({ root: "test-root" });
73
+ expect(renderMissingLink).toHaveBeenCalled();
74
+ expect(renderConfirmationPrompt).toHaveBeenCalled();
75
+ expect(linkStorefront).toHaveBeenCalled();
76
+ });
77
+ it("returns nothing if the user does not create a new link", async () => {
78
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(false);
79
+ const token = await getOxygenDeploymentToken({ root: "test-root" });
80
+ expect(token).toEqual(void 0);
81
+ });
82
+ });
83
+ describe("when there is no matching storefront in the shop", () => {
84
+ beforeEach(() => {
85
+ vi.mocked(getOxygenToken).mockResolvedValue({ storefront: null });
86
+ });
87
+ it("calls renderMissingStorefront and returns nothing", async () => {
88
+ const token = await getOxygenDeploymentToken({ root: "test-root" });
89
+ expect(renderMissingStorefront).toHaveBeenCalled();
90
+ expect(token).toEqual(void 0);
91
+ });
92
+ });
93
+ describe("when the storefront does not have an oxygen deployment token", () => {
94
+ beforeEach(() => {
95
+ vi.mocked(getOxygenToken).mockResolvedValue({
96
+ storefront: { oxygenDeploymentToken: "" }
97
+ });
98
+ });
99
+ it("returns nothing", async () => {
100
+ const token = await getOxygenDeploymentToken({ root: "test-root" });
101
+ expect(token).toEqual(void 0);
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,21 @@
1
+ import { adminRequest } from './client.js';
2
+
3
+ const GetDeploymentTokenQuery = `#graphql
4
+ query GetDeploymentToken($id: ID!) {
5
+ hydrogenStorefront(id: $id) {
6
+ oxygenDeploymentToken
7
+ }
8
+ }
9
+ `;
10
+ async function getOxygenToken(adminSession, storefrontId) {
11
+ const { hydrogenStorefront } = await adminRequest(
12
+ GetDeploymentTokenQuery,
13
+ adminSession,
14
+ {
15
+ id: storefrontId
16
+ }
17
+ );
18
+ return { storefront: hydrogenStorefront };
19
+ }
20
+
21
+ export { GetDeploymentTokenQuery, getOxygenToken };
package/dist/lib/log.js CHANGED
@@ -37,10 +37,17 @@ function injectLogReplacer(method, debouncer) {
37
37
  console[method] = (...args) => {
38
38
  if (debounceMessage(args, debouncer?.(args)))
39
39
  return;
40
- const replacer = messageReplacers.find(([matcher]) => matcher(args))?.[1];
41
- if (!replacer)
40
+ const replacers = messageReplacers.reduce((acc, [matcher, replacer]) => {
41
+ if (matcher(args))
42
+ acc.push(replacer);
43
+ return acc;
44
+ }, []);
45
+ if (replacers.length === 0)
42
46
  return originalConsole[method](...args);
43
- const result = replacer(args);
47
+ const result = replacers.reduce(
48
+ (resultArgs, replacer) => resultArgs && replacer(resultArgs),
49
+ args
50
+ );
44
51
  if (result)
45
52
  return originalConsole[method](...result);
46
53
  };
@@ -48,8 +55,10 @@ function injectLogReplacer(method, debouncer) {
48
55
  }
49
56
  function muteDevLogs({ workerReload } = {}) {
50
57
  injectLogReplacer("log");
58
+ injectLogReplacer("error");
59
+ injectLogReplacer("warn");
51
60
  let isFirstWorkerReload = true;
52
- addMessageReplacers("dev", [
61
+ addMessageReplacers("dev-node", [
53
62
  ([first]) => typeof first === "string" && first.includes("[mf:"),
54
63
  (args) => {
55
64
  const first = args[0];
@@ -65,6 +74,29 @@ function muteDevLogs({ workerReload } = {}) {
65
74
  }
66
75
  }
67
76
  ]);
77
+ addMessageReplacers(
78
+ "dev-workerd",
79
+ [
80
+ // Workerd logs
81
+ ([first]) => typeof first === "string" && /^\x1B\[31m(workerd\/|stack:)/.test(first),
82
+ () => {
83
+ }
84
+ ],
85
+ // Non-actionable warnings/errors:
86
+ [
87
+ ([first]) => typeof first === "string" && /^A promise rejection/i.test(first),
88
+ () => {
89
+ }
90
+ ],
91
+ [
92
+ ([first]) => {
93
+ const message = first?.message ?? first;
94
+ return typeof message === "string" && /^Network connection lost/i.test(message);
95
+ },
96
+ () => {
97
+ }
98
+ ]
99
+ );
68
100
  }
69
101
  const originalWrite = process.stdout.write;
70
102
  function muteAuthLogs({
@@ -76,12 +108,17 @@ function muteAuthLogs({
76
108
  process.stdout.write = (item, cb) => {
77
109
  if (typeof item !== "string")
78
110
  return write(item, cb);
79
- const replacer = messageReplacers.find(
80
- ([matcher]) => matcher([item])
81
- )?.[1];
82
- if (!replacer)
111
+ const replacers = messageReplacers.reduce((acc, [matcher, replacer]) => {
112
+ if (matcher([item]))
113
+ acc.push(replacer);
114
+ return acc;
115
+ }, []);
116
+ if (replacers.length === 0)
83
117
  return write(item, cb);
84
- const result = replacer([item]);
118
+ const result = replacers.reduce(
119
+ (resultArgs, replacer) => resultArgs && replacer(resultArgs),
120
+ [item]
121
+ );
85
122
  if (result)
86
123
  return write(result[0], cb);
87
124
  };
@@ -129,13 +166,13 @@ function enhanceH2Logs(options) {
129
166
  addMessageReplacers("h2-warn", [
130
167
  ([first]) => {
131
168
  const message = first?.message ?? first;
132
- return typeof message === "string" && message.startsWith("[h2:");
169
+ return typeof message === "string" && message.includes("[h2:");
133
170
  },
134
171
  (args) => {
135
172
  const firstArg = args[0];
136
173
  const errorObject = typeof firstArg === "object" && !!firstArg.stack ? firstArg : void 0;
137
174
  const stringArg = errorObject?.message ?? firstArg;
138
- const [, type, scope, message] = stringArg.match(/^\[h2:([^:]+):([^\]]+)\]\s+(.*)$/ims) || [];
175
+ const [, type, scope, message] = stringArg.match(/\[h2:([^:]+):([^\]]+)\]\s+(.*)$/ims) || [];
139
176
  if (!type || !scope || !message)
140
177
  return args;
141
178
  const headline = `In Hydrogen's \`${scope.trim()}\`:
@@ -149,8 +186,14 @@ function enhanceH2Logs(options) {
149
186
  if (type === "error" || errorObject) {
150
187
  let tryMessage = hasLinks ? lastLine : void 0;
151
188
  let stack = errorObject?.stack;
152
- const cause = errorObject?.cause;
153
- if (!!cause?.graphql?.query) {
189
+ let cause = errorObject?.cause;
190
+ if (typeof cause === "string") {
191
+ try {
192
+ cause = JSON.parse(cause);
193
+ } catch {
194
+ }
195
+ }
196
+ if (typeof cause !== "string" && !!cause?.graphql?.query) {
154
197
  const { query, variables } = cause.graphql;
155
198
  const link = `${options.graphiqlUrl}?query=${encodeURIComponent(
156
199
  query
@@ -0,0 +1,58 @@
1
+ import { outputToken, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
2
+ import colors from '@shopify/cli-kit/node/colors';
3
+ import { DEV_ROUTES } from '../request-events.js';
4
+
5
+ function logRequestLine(request, {
6
+ responseStatus = 200,
7
+ durationMs = 0
8
+ } = {}) {
9
+ try {
10
+ const url = new URL(request.url);
11
+ if (DEV_ROUTES.has(url.pathname))
12
+ return;
13
+ const isDataRequest = url.searchParams.has("_data");
14
+ let route = request.url.replace(url.origin, "");
15
+ let info = "";
16
+ let type = "render";
17
+ if (isDataRequest) {
18
+ type = request.method === "GET" ? "loader" : "action";
19
+ const dataParam = url.searchParams.get("_data")?.replace("routes/", "");
20
+ route = url.pathname;
21
+ info = `[${dataParam}]`;
22
+ }
23
+ const colorizeStatus = responseStatus < 300 ? outputToken.green : responseStatus < 400 ? outputToken.cyan : outputToken.errorText;
24
+ outputInfo(
25
+ outputContent`${request.method.padStart(6)} ${colorizeStatus(
26
+ String(responseStatus)
27
+ )} ${outputToken.italic(type.padEnd(7, " "))} ${route} ${durationMs > 0 ? colors.dim(` ${durationMs}ms`) : ""}${info ? " " + colors.dim(info) : ""}${request.headers.get("purpose") === "prefetch" ? outputToken.italic(colors.dim(" prefetch")) : ""}`
28
+ );
29
+ } catch {
30
+ if (request && responseStatus) {
31
+ outputInfo(`${request.method} ${responseStatus} ${request.url}`);
32
+ }
33
+ }
34
+ }
35
+ const OXYGEN_HEADERS_MAP = {
36
+ ip: { name: "oxygen-buyer-ip", defaultValue: "127.0.0.1" },
37
+ longitude: { name: "oxygen-buyer-longitude", defaultValue: "-122.40140" },
38
+ latitude: { name: "oxygen-buyer-latitude", defaultValue: "37.78855" },
39
+ continent: { name: "oxygen-buyer-continent", defaultValue: "NA" },
40
+ country: { name: "oxygen-buyer-country", defaultValue: "US" },
41
+ region: { name: "oxygen-buyer-region", defaultValue: "California" },
42
+ regionCode: { name: "oxygen-buyer-region-code", defaultValue: "CA" },
43
+ city: { name: "oxygen-buyer-city", defaultValue: "San Francisco" },
44
+ isEuCountry: { name: "oxygen-buyer-is-eu-country", defaultValue: "" },
45
+ timezone: {
46
+ name: "oxygen-buyer-timezone",
47
+ defaultValue: "America/Los_Angeles"
48
+ },
49
+ // Not documented but available in Oxygen:
50
+ deploymentId: { name: "oxygen-buyer-deployment-id", defaultValue: "local" },
51
+ shopId: { name: "oxygen-buyer-shop-id", defaultValue: "development" },
52
+ storefrontId: {
53
+ name: "oxygen-buyer-storefront-id",
54
+ defaultValue: "development"
55
+ }
56
+ };
57
+
58
+ export { OXYGEN_HEADERS_MAP, logRequestLine };
@@ -0,0 +1,12 @@
1
+ async function startMiniOxygen(options, useWorkerd = false) {
2
+ if (useWorkerd) {
3
+ const { startWorkerdServer } = await import('./workerd.js');
4
+ return startWorkerdServer(options);
5
+ } else {
6
+ process.env.MINIFLARE_SUBREQUEST_LIMIT = 100;
7
+ const { startNodeServer } = await import('./node.js');
8
+ return startNodeServer(options);
9
+ }
10
+ }
11
+
12
+ export { startMiniOxygen };
@@ -1,24 +1,27 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
- import { outputToken, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
4
3
  import { resolvePath } from '@shopify/cli-kit/node/path';
5
- import { readFile, fileExists } from '@shopify/cli-kit/node/fs';
6
- import colors from '@shopify/cli-kit/node/colors';
4
+ import { readFile } from '@shopify/cli-kit/node/fs';
7
5
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
8
6
  import { startServer, Request } from '@shopify/mini-oxygen';
9
- import { DEFAULT_PORT } from './flags.js';
10
- import { clearHistory, streamRequestEvents, DEV_ROUTES, logRequestEvent } from './request-events.js';
7
+ import { DEFAULT_PORT } from '../flags.js';
8
+ import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
9
+ import { clearHistory, streamRequestEvents, logRequestEvent } from '../request-events.js';
11
10
 
12
- async function startMiniOxygen({
11
+ async function startNodeServer({
13
12
  root,
14
13
  port = DEFAULT_PORT,
15
14
  watch = false,
16
- autoReload = watch,
17
15
  buildPathWorkerFile,
18
16
  buildPathClient,
19
17
  env
20
18
  }) {
21
- const dotenvPath = resolvePath(root, ".env");
19
+ resolvePath(root, ".env");
20
+ const oxygenHeaders = Object.fromEntries(
21
+ Object.entries(OXYGEN_HEADERS_MAP).map(([key, value]) => {
22
+ return [key, value.defaultValue];
23
+ })
24
+ );
22
25
  const asyncLocalStorage = new AsyncLocalStorage();
23
26
  const serviceBindings = {
24
27
  H2O_LOG_EVENT: {
@@ -40,16 +43,16 @@ async function startMiniOxygen({
40
43
  publicPath: "",
41
44
  port,
42
45
  watch,
43
- autoReload,
46
+ autoReload: watch,
44
47
  modules: true,
45
48
  env: {
46
49
  ...env,
47
50
  ...process.env,
48
51
  ...serviceBindings
49
52
  },
50
- envPath: !env && await fileExists(dotenvPath) ? dotenvPath : void 0,
51
53
  log: () => {
52
54
  },
55
+ oxygenHeaders,
53
56
  async onRequest(request, defaultDispatcher) {
54
57
  const url = new URL(request.url);
55
58
  if (url.pathname === "/debug-network-server") {
@@ -60,11 +63,15 @@ async function startMiniOxygen({
60
63
  requestId = randomUUID();
61
64
  request.headers.set("request-id", requestId);
62
65
  }
66
+ const startTimeMs = Date.now();
63
67
  const response = await asyncLocalStorage.run(
64
68
  { "request-id": requestId, purpose: request.headers.get("purpose") },
65
69
  () => defaultDispatcher(request)
66
70
  );
67
- logResponse(request, response);
71
+ logRequestLine(request, {
72
+ responseStatus: response.status,
73
+ durationMs: startTimeMs > 0 ? Date.now() - startTimeMs : 0
74
+ });
68
75
  return response;
69
76
  }
70
77
  });
@@ -72,19 +79,16 @@ async function startMiniOxygen({
72
79
  return {
73
80
  listeningAt,
74
81
  port: miniOxygen.port,
75
- async reload(options = {}) {
82
+ async reload(options) {
76
83
  const nextOptions = {};
77
- if (options.env) {
84
+ if (options?.env) {
78
85
  nextOptions.env = {
79
86
  ...options.env,
80
- ...process.env,
81
- ...serviceBindings
87
+ ...process.env
82
88
  };
83
89
  }
84
- if (options.worker) {
85
- nextOptions.script = await readFile(buildPathWorkerFile);
86
- }
87
- return miniOxygen.reload(nextOptions);
90
+ nextOptions.script = await readFile(buildPathWorkerFile);
91
+ await miniOxygen.reload(nextOptions);
88
92
  },
89
93
  showBanner(options) {
90
94
  console.log("");
@@ -96,40 +100,11 @@ async function startMiniOxygen({
96
100
  ]
97
101
  });
98
102
  console.log("");
103
+ },
104
+ async close() {
105
+ await miniOxygen.close();
99
106
  }
100
107
  };
101
108
  }
102
- function logResponse(request, response) {
103
- try {
104
- const url = new URL(request.url);
105
- if (DEV_ROUTES.has(url.pathname))
106
- return;
107
- const isProxy = !!response.url && response.url !== request.url;
108
- const isDataRequest = !isProxy && url.searchParams.has("_data");
109
- let route = request.url.replace(url.origin, "");
110
- let info = "";
111
- let type = "render";
112
- if (isProxy) {
113
- type = "proxy";
114
- info = `[${response.url}]`;
115
- }
116
- if (isDataRequest) {
117
- type = request.method === "GET" ? "loader" : "action";
118
- const dataParam = url.searchParams.get("_data")?.replace("routes/", "");
119
- route = url.pathname;
120
- info = `[${dataParam}]`;
121
- }
122
- const colorizeStatus = response.status < 300 ? outputToken.green : response.status < 400 ? outputToken.cyan : outputToken.errorText;
123
- outputInfo(
124
- outputContent`${request.method.padStart(6)} ${colorizeStatus(
125
- String(response.status)
126
- )} ${outputToken.italic(type.padEnd(7, " "))} ${route}${info ? " " + colors.dim(info) : ""} ${request.headers.get("purpose") === "prefetch" ? outputToken.italic("(prefetch)") : ""}`
127
- );
128
- } catch {
129
- if (request && response) {
130
- outputInfo(`${request.method} ${response.status} ${request.url}`);
131
- }
132
- }
133
- }
134
109
 
135
- export { logResponse, startMiniOxygen };
110
+ export { startNodeServer };
@@ -0,0 +1 @@
1
+