@shopify/cli-hydrogen 7.1.0 → 7.1.2
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-vite.js +131 -0
- package/dist/commands/hydrogen/build.js +6 -15
- package/dist/commands/hydrogen/check.js +1 -1
- package/dist/commands/hydrogen/codegen.js +3 -3
- package/dist/commands/hydrogen/debug/cpu.js +1 -1
- package/dist/commands/hydrogen/deploy.js +46 -31
- package/dist/commands/hydrogen/deploy.test.js +35 -49
- package/dist/commands/hydrogen/dev-vite.js +159 -0
- package/dist/commands/hydrogen/dev.js +11 -14
- package/dist/commands/hydrogen/env/list.js +1 -1
- package/dist/commands/hydrogen/env/pull.js +3 -3
- package/dist/commands/hydrogen/env/pull.test.js +2 -0
- package/dist/commands/hydrogen/env/push__unstable.js +190 -0
- package/dist/commands/hydrogen/env/push__unstable.test.js +383 -0
- package/dist/commands/hydrogen/generate/route.js +2 -2
- package/dist/commands/hydrogen/init.d.ts +69 -0
- package/dist/commands/hydrogen/init.js +5 -5
- package/dist/commands/hydrogen/init.test.js +2 -2
- package/dist/commands/hydrogen/link.js +2 -2
- package/dist/commands/hydrogen/list.js +1 -1
- package/dist/commands/hydrogen/login.js +2 -9
- package/dist/commands/hydrogen/logout.js +1 -1
- package/dist/commands/hydrogen/preview.js +15 -7
- package/dist/commands/hydrogen/setup/css.js +3 -3
- package/dist/commands/hydrogen/setup/markets.js +4 -4
- package/dist/commands/hydrogen/setup/vite.js +209 -0
- package/dist/commands/hydrogen/setup.js +8 -6
- package/dist/commands/hydrogen/unlink.js +1 -1
- package/dist/commands/hydrogen/upgrade.js +5 -3
- package/dist/generator-templates/assets/vite/package.json +15 -0
- package/dist/generator-templates/assets/vite/vite.config.js +13 -0
- package/dist/generator-templates/starter/CHANGELOG.md +49 -0
- package/dist/generator-templates/starter/app/components/Search.tsx +12 -7
- package/dist/generator-templates/starter/app/root.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +8 -15
- package/dist/generator-templates/starter/package.json +9 -8
- package/dist/generator-templates/starter/public/.gitkeep +0 -0
- package/dist/lib/build.js +2 -1
- package/dist/lib/codegen.js +8 -3
- package/dist/lib/environment-variables.test.js +4 -2
- package/dist/lib/flags.js +149 -95
- package/dist/lib/graphql/admin/pull-variables.js +1 -0
- package/dist/lib/graphql/admin/pull-variables.test.js +7 -1
- package/dist/lib/graphql/admin/push-variables.js +35 -0
- package/dist/lib/log.js +1 -0
- package/dist/lib/mini-oxygen/common.js +2 -1
- package/dist/lib/mini-oxygen/node.js +2 -2
- package/dist/lib/mini-oxygen/workerd-inspector.js +1 -1
- package/dist/lib/mini-oxygen/workerd.js +29 -17
- package/dist/lib/onboarding/common.js +0 -3
- package/dist/lib/onboarding/local.js +4 -1
- package/dist/lib/onboarding/remote.js +16 -11
- package/dist/lib/remix-config.js +1 -1
- package/dist/lib/request-events.js +3 -3
- package/dist/lib/setups/css/assets.js +7 -2
- package/dist/lib/template-diff.js +26 -11
- package/dist/lib/template-downloader.js +11 -2
- package/dist/lib/vite/hydrogen-middleware.js +82 -0
- package/dist/lib/vite/mini-oxygen.js +152 -0
- package/dist/lib/vite/plugins.d.ts +27 -0
- package/dist/lib/vite/plugins.js +139 -0
- package/dist/lib/vite/shared.js +10 -0
- package/dist/lib/vite/utils.js +55 -0
- package/dist/lib/vite/worker-entry.js +1518 -0
- package/dist/lib/vite-config.js +45 -0
- package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +4 -2
- package/dist/virtual-routes/routes/index.jsx +5 -5
- package/dist/virtual-routes/routes/subrequest-profiler.jsx +1 -1
- package/dist/virtual-routes/virtual-root.jsx +1 -1
- package/oclif.manifest.json +1127 -494
- package/package.json +36 -11
- /package/dist/generator-templates/starter/{public → app/assets}/favicon.svg +0 -0
|
@@ -3,6 +3,7 @@ import { temporaryDirectory } from 'tempy';
|
|
|
3
3
|
import { createSymlink, copy, remove } from 'fs-extra/esm';
|
|
4
4
|
import { copyFile, removeFile } from '@shopify/cli-kit/node/fs';
|
|
5
5
|
import { joinPath, relativePath } from '@shopify/cli-kit/node/path';
|
|
6
|
+
import { readAndParsePackageJson } from '@shopify/cli-kit/node/node-package-manager';
|
|
6
7
|
import colors from '@shopify/cli-kit/node/colors';
|
|
7
8
|
import { getRepoNodeModules, getStarterDir } from './build.js';
|
|
8
9
|
import { mergePackageJson } from './file.js';
|
|
@@ -65,35 +66,49 @@ ${colors.dim(
|
|
|
65
66
|
return targetDirectory;
|
|
66
67
|
}
|
|
67
68
|
async function applyTemplateDiff(targetDirectory, diffDirectory, templateDir = getStarterDir()) {
|
|
68
|
-
const
|
|
69
|
+
const pkgJson = await readAndParsePackageJson(
|
|
70
|
+
joinPath(diffDirectory, "package.json")
|
|
71
|
+
);
|
|
72
|
+
const createFilter = (re, skipFiles) => (filepath) => {
|
|
73
|
+
const filename = relativePath(templateDir, filepath);
|
|
74
|
+
return !re.test(filename) && !skipFiles?.includes(filename);
|
|
75
|
+
};
|
|
69
76
|
await copy(templateDir, targetDirectory, {
|
|
70
77
|
filter: createFilter(
|
|
71
|
-
/(^|\/|\\)(dist|node_modules|\.cache|CHANGELOG\.md)(\/|\\|$)/i
|
|
78
|
+
/(^|\/|\\)(dist|node_modules|\.cache|.turbo|CHANGELOG\.md)(\/|\\|$)/i,
|
|
79
|
+
pkgJson["h2:diff"]?.["skip-files"] || []
|
|
72
80
|
)
|
|
73
81
|
});
|
|
74
82
|
await copy(diffDirectory, targetDirectory, {
|
|
75
83
|
filter: createFilter(
|
|
76
|
-
/(^|\/|\\)(dist|node_modules|\.cache|package\.json|tsconfig\.json)(\/|\\|$)/i
|
|
84
|
+
/(^|\/|\\)(dist|node_modules|\.cache|.turbo|package\.json|tsconfig\.json)(\/|\\|$)/i
|
|
77
85
|
)
|
|
78
86
|
});
|
|
79
87
|
await mergePackageJson(diffDirectory, targetDirectory, {
|
|
80
|
-
|
|
88
|
+
ignoredKeys: ["h2:diff"],
|
|
89
|
+
onResult: (pkgJson2) => {
|
|
81
90
|
for (const key of ["build", "dev"]) {
|
|
82
|
-
const scriptLine =
|
|
83
|
-
if (
|
|
84
|
-
|
|
91
|
+
const scriptLine = pkgJson2.scripts?.[key];
|
|
92
|
+
if (pkgJson2.scripts?.[key] && typeof scriptLine === "string") {
|
|
93
|
+
pkgJson2.scripts[key] = scriptLine.replace(/\s+--diff/, "");
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
|
-
return
|
|
96
|
+
return pkgJson2;
|
|
88
97
|
}
|
|
89
98
|
});
|
|
90
99
|
}
|
|
91
100
|
async function copyDiffBuild(targetDirectory, diffDirectory) {
|
|
92
101
|
const targetDist = joinPath(diffDirectory, "dist");
|
|
93
102
|
await remove(targetDist);
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
await Promise.all([
|
|
104
|
+
copy(joinPath(targetDirectory, "dist"), targetDist, {
|
|
105
|
+
overwrite: true
|
|
106
|
+
}),
|
|
107
|
+
copyFile(
|
|
108
|
+
joinPath(targetDirectory, ".env"),
|
|
109
|
+
joinPath(diffDirectory, ".env")
|
|
110
|
+
)
|
|
111
|
+
]);
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
export { applyTemplateDiff, copyDiffBuild, prepareDiffDirectory };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import path from 'path';
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { pipeline } from 'stream/promises';
|
|
3
4
|
import gunzipMaybe from 'gunzip-maybe';
|
|
4
5
|
import { extract } from 'tar-fs';
|
|
5
6
|
import { fetch } from '@shopify/cli-kit/node/http';
|
|
6
7
|
import { fileExists, mkdir } from '@shopify/cli-kit/node/fs';
|
|
7
8
|
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
8
|
-
import {
|
|
9
|
+
import { getSkeletonSourceDir } from './build.js';
|
|
9
10
|
|
|
10
11
|
const REPO_RELEASES_URL = `https://api.github.com/repos/shopify/hydrogen/releases/latest`;
|
|
11
12
|
const getTryMessage = (status) => status === 403 ? `If you are using a VPN, WARP, or similar service, consider disabling it momentarily.` : void 0;
|
|
@@ -50,6 +51,14 @@ async function downloadTarball(url, storageDir, signal) {
|
|
|
50
51
|
async function getLatestTemplates({
|
|
51
52
|
signal
|
|
52
53
|
} = {}) {
|
|
54
|
+
if (process.env.LOCAL_DEV) {
|
|
55
|
+
const templatesDir = path.dirname(getSkeletonSourceDir());
|
|
56
|
+
return {
|
|
57
|
+
version: "local",
|
|
58
|
+
templatesDir,
|
|
59
|
+
examplesDir: path.resolve(templatesDir, "..", "examples")
|
|
60
|
+
};
|
|
61
|
+
}
|
|
53
62
|
try {
|
|
54
63
|
const { version, url } = await getLatestReleaseDownloadUrl(signal);
|
|
55
64
|
const templateStoragePath = fileURLToPath(
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { normalizePath } from 'vite';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { createFileReadStream } from '@shopify/cli-kit/node/fs';
|
|
5
|
+
import { setConstructors, handleDebugNetworkRequest } from '../request-events.js';
|
|
6
|
+
import { SUBREQUEST_PROFILER_ENDPOINT } from '../mini-oxygen/common.js';
|
|
7
|
+
import { toWeb, pipeFromWeb } from './utils.js';
|
|
8
|
+
import { addVirtualRoutes } from '../virtual-routes.js';
|
|
9
|
+
|
|
10
|
+
function setupRemixDevServerHooks(viteUrl) {
|
|
11
|
+
globalThis["__remix_devServerHooks"] = {
|
|
12
|
+
getCriticalCss: (...args) => fetch(new URL("/__vite_critical_css", viteUrl), {
|
|
13
|
+
method: "POST",
|
|
14
|
+
body: JSON.stringify(args)
|
|
15
|
+
}).then((res) => res.json())
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function setupHydrogenMiddleware(viteDevServer, options) {
|
|
19
|
+
viteDevServer.middlewares.use(
|
|
20
|
+
"/__vite_critical_css",
|
|
21
|
+
function h2HandleCriticalCss(req, res) {
|
|
22
|
+
toWeb(req).json().then(async (args) => {
|
|
23
|
+
const result = await globalThis["__remix_devServerHooks"]?.getCriticalCss?.(...args);
|
|
24
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
25
|
+
res.end(JSON.stringify(result ?? ""));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
if (options.disableVirtualRoutes)
|
|
30
|
+
return;
|
|
31
|
+
addVirtualRoutesToRemix(viteDevServer);
|
|
32
|
+
setConstructors({ Response: globalThis.Response });
|
|
33
|
+
viteDevServer.middlewares.use(
|
|
34
|
+
SUBREQUEST_PROFILER_ENDPOINT,
|
|
35
|
+
function h2HandleSubrequestProfilerEvent(req, res) {
|
|
36
|
+
const webResponse = handleDebugNetworkRequest(toWeb(req));
|
|
37
|
+
pipeFromWeb(webResponse, res);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
viteDevServer.middlewares.use(
|
|
41
|
+
"/graphiql/customer-account.schema.json",
|
|
42
|
+
function h2HandleGraphiQLCustomerSchema(req, res) {
|
|
43
|
+
const require2 = createRequire(import.meta.url);
|
|
44
|
+
const filePath = require2.resolve(
|
|
45
|
+
"@shopify/hydrogen/customer-account.schema.json"
|
|
46
|
+
);
|
|
47
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
48
|
+
createFileReadStream(filePath).pipe(res);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
let virtualRoutesAdded = false;
|
|
53
|
+
async function addVirtualRoutesToRemix(viteDevServer) {
|
|
54
|
+
if (virtualRoutesAdded)
|
|
55
|
+
return;
|
|
56
|
+
const appDirectory = await reloadRemixVirtualRoutes(viteDevServer.config);
|
|
57
|
+
viteDevServer.watcher.on("all", (eventName, filepath) => {
|
|
58
|
+
const appFileAddedOrRemoved = (eventName === "add" || eventName === "unlink") && normalizePath(filepath).startsWith(normalizePath(appDirectory));
|
|
59
|
+
const viteConfigChanged = eventName === "change" && normalizePath(filepath) === normalizePath(viteDevServer.config.configFile ?? "");
|
|
60
|
+
if (appFileAddedOrRemoved || viteConfigChanged) {
|
|
61
|
+
setTimeout(() => reloadRemixVirtualRoutes(viteDevServer.config), 100);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
virtualRoutesAdded = true;
|
|
65
|
+
}
|
|
66
|
+
async function reloadRemixVirtualRoutes(config) {
|
|
67
|
+
const remixPluginContext = config.__remixPluginContext;
|
|
68
|
+
remixPluginContext.remixConfig = { ...remixPluginContext.remixConfig };
|
|
69
|
+
remixPluginContext.remixConfig.routes = {
|
|
70
|
+
...remixPluginContext.remixConfig.routes
|
|
71
|
+
};
|
|
72
|
+
await addVirtualRoutes(remixPluginContext.remixConfig).catch((error) => {
|
|
73
|
+
console.debug(
|
|
74
|
+
"Could not add virtual routes: " + (error?.stack ?? error?.message ?? error)
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
Object.freeze(remixPluginContext.remixConfig.routes);
|
|
78
|
+
Object.freeze(remixPluginContext.remixConfig);
|
|
79
|
+
return remixPluginContext?.remixConfig?.appDirectory ?? path.join(config.root, "app");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { setupHydrogenMiddleware, setupRemixDevServerHooks };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { fetchModule } from 'vite';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { Miniflare, NoOpLog } from 'miniflare';
|
|
5
|
+
import { OXYGEN_HEADERS_MAP, logRequestLine } from '../mini-oxygen/common.js';
|
|
6
|
+
import { PRIVATE_WORKERD_INSPECTOR_PORT, OXYGEN_WORKERD_COMPAT_PARAMS } from '../mini-oxygen/workerd.js';
|
|
7
|
+
import { findPort } from '../find-port.js';
|
|
8
|
+
import { createInspectorConnector } from '../mini-oxygen/workerd-inspector.js';
|
|
9
|
+
import { getHmrUrl, toURL, toWeb, pipeFromWeb } from './utils.js';
|
|
10
|
+
|
|
11
|
+
const scriptPath = fileURLToPath(new URL("./worker-entry.js", import.meta.url));
|
|
12
|
+
const FETCH_MODULE_PATHNAME = "/__vite_fetch_module";
|
|
13
|
+
const WARMUP_PATHNAME = "/__vite_warmup";
|
|
14
|
+
const oxygenHeadersMap = Object.values(OXYGEN_HEADERS_MAP).reduce(
|
|
15
|
+
(acc, item) => {
|
|
16
|
+
acc[item.name] = item.defaultValue;
|
|
17
|
+
return acc;
|
|
18
|
+
},
|
|
19
|
+
{}
|
|
20
|
+
);
|
|
21
|
+
async function startMiniOxygenRuntime({
|
|
22
|
+
viteDevServer,
|
|
23
|
+
env,
|
|
24
|
+
services,
|
|
25
|
+
debug = false,
|
|
26
|
+
inspectorPort,
|
|
27
|
+
workerEntryFile,
|
|
28
|
+
setupScripts
|
|
29
|
+
}) {
|
|
30
|
+
const [publicInspectorPort, privateInspectorPort] = await Promise.all([
|
|
31
|
+
findPort(inspectorPort),
|
|
32
|
+
findPort(PRIVATE_WORKERD_INSPECTOR_PORT)
|
|
33
|
+
]);
|
|
34
|
+
const mf = new Miniflare({
|
|
35
|
+
cf: false,
|
|
36
|
+
verbose: false,
|
|
37
|
+
log: new NoOpLog(),
|
|
38
|
+
inspectorPort: privateInspectorPort,
|
|
39
|
+
handleRuntimeStdio(stdout, stderr) {
|
|
40
|
+
stdout.destroy();
|
|
41
|
+
stderr.destroy();
|
|
42
|
+
},
|
|
43
|
+
workers: [
|
|
44
|
+
{
|
|
45
|
+
name: "oxygen",
|
|
46
|
+
modulesRoot: "/",
|
|
47
|
+
modules: [{ type: "ESModule", path: scriptPath }],
|
|
48
|
+
...OXYGEN_WORKERD_COMPAT_PARAMS,
|
|
49
|
+
serviceBindings: { ...services },
|
|
50
|
+
bindings: {
|
|
51
|
+
...env,
|
|
52
|
+
__VITE_ROOT: viteDevServer.config.root,
|
|
53
|
+
__VITE_RUNTIME_EXECUTE_URL: workerEntryFile,
|
|
54
|
+
__VITE_FETCH_MODULE_PATHNAME: FETCH_MODULE_PATHNAME,
|
|
55
|
+
__VITE_HMR_URL: getHmrUrl(viteDevServer),
|
|
56
|
+
__VITE_WARMUP_PATHNAME: WARMUP_PATHNAME
|
|
57
|
+
},
|
|
58
|
+
unsafeEvalBinding: "__VITE_UNSAFE_EVAL",
|
|
59
|
+
wrappedBindings: {
|
|
60
|
+
__VITE_SETUP_ENV: "setup-environment"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "setup-environment",
|
|
65
|
+
modules: true,
|
|
66
|
+
scriptPath,
|
|
67
|
+
script: `
|
|
68
|
+
const setupScripts = [${setupScripts ?? ""}];
|
|
69
|
+
export default (env) => (request) => {
|
|
70
|
+
const viteUrl = new URL(request.url).origin;
|
|
71
|
+
setupScripts.forEach((setup) => setup?.(viteUrl));
|
|
72
|
+
setupScripts.length = 0;
|
|
73
|
+
}`
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
const warmupWorkerdCache = () => {
|
|
78
|
+
let viteUrl = viteDevServer.resolvedUrls?.local[0] ?? viteDevServer.resolvedUrls?.network[0];
|
|
79
|
+
if (!viteUrl) {
|
|
80
|
+
const address = viteDevServer.httpServer?.address?.();
|
|
81
|
+
viteUrl = address && typeof address !== "string" ? `http://localhost:${address.port}` : address ?? void 0;
|
|
82
|
+
}
|
|
83
|
+
if (viteUrl) {
|
|
84
|
+
mf.dispatchFetch(new URL(WARMUP_PATHNAME, viteUrl)).catch(() => {
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
viteDevServer.httpServer?.listening ? warmupWorkerdCache() : viteDevServer.httpServer?.once("listening", warmupWorkerdCache);
|
|
89
|
+
mf.ready.then(() => {
|
|
90
|
+
const reconnect = createInspectorConnector({
|
|
91
|
+
debug,
|
|
92
|
+
sourceMapPath: "",
|
|
93
|
+
absoluteBundlePath: "",
|
|
94
|
+
privateInspectorPort,
|
|
95
|
+
publicInspectorPort
|
|
96
|
+
});
|
|
97
|
+
return reconnect();
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
ready: mf.ready,
|
|
101
|
+
publicInspectorPort,
|
|
102
|
+
dispatch: (webRequest) => mf.dispatchFetch(webRequest),
|
|
103
|
+
async dispose() {
|
|
104
|
+
await mf.dispose();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function setupOxygenMiddleware(viteDevServer, dispatchFetch) {
|
|
109
|
+
viteDevServer.middlewares.use(
|
|
110
|
+
FETCH_MODULE_PATHNAME,
|
|
111
|
+
function o2HandleModuleFetch(req, res) {
|
|
112
|
+
const url = toURL(req);
|
|
113
|
+
const id = url.searchParams.get("id");
|
|
114
|
+
const importer = url.searchParams.get("importer") ?? void 0;
|
|
115
|
+
if (id) {
|
|
116
|
+
res.setHeader("cache-control", "no-store");
|
|
117
|
+
res.setHeader("content-type", "application/json");
|
|
118
|
+
fetchModule(viteDevServer, id, importer).then((ssrModule) => res.end(JSON.stringify(ssrModule))).catch((error) => {
|
|
119
|
+
console.error("Error during module fetch:", error);
|
|
120
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
121
|
+
res.end("Internal server error");
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
res.statusCode = 400;
|
|
125
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
126
|
+
res.end("Invalid request");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
viteDevServer.middlewares.use(function o2HandleWorkerRequest(req, res) {
|
|
131
|
+
if (!req.headers.host)
|
|
132
|
+
throw new Error("Missing host header");
|
|
133
|
+
const webRequest = toWeb(req, {
|
|
134
|
+
"request-id": crypto.randomUUID(),
|
|
135
|
+
...oxygenHeadersMap
|
|
136
|
+
});
|
|
137
|
+
const startTimeMs = Date.now();
|
|
138
|
+
dispatchFetch(webRequest).then((webResponse) => {
|
|
139
|
+
pipeFromWeb(webResponse, res);
|
|
140
|
+
logRequestLine(webRequest, {
|
|
141
|
+
responseStatus: webResponse.status,
|
|
142
|
+
durationMs: Date.now() - startTimeMs
|
|
143
|
+
});
|
|
144
|
+
}).catch((error) => {
|
|
145
|
+
console.error("Error during evaluation:", error);
|
|
146
|
+
res.writeHead(500);
|
|
147
|
+
res.end();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { setupOxygenMiddleware, startMiniOxygenRuntime };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
type HydrogenPluginOptions = {
|
|
4
|
+
disableVirtualRoutes?: boolean;
|
|
5
|
+
};
|
|
6
|
+
type OxygenPluginOptions = {
|
|
7
|
+
ssrEntry?: string;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
inspectorPort?: number;
|
|
10
|
+
env?: Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Enables Hydrogen utilities for local development
|
|
15
|
+
* such as GraphiQL, Subrequest Profiler, etc.
|
|
16
|
+
* It must be used in combination with the `oxygen` plugin and Hydrogen CLI.
|
|
17
|
+
* @experimental
|
|
18
|
+
*/
|
|
19
|
+
declare function hydrogen(pluginOptions?: HydrogenPluginOptions): Plugin[];
|
|
20
|
+
/**
|
|
21
|
+
* Runs backend code in an Oxygen worker instead of Node.js during development.
|
|
22
|
+
* It must be placed after `hydrogen` but before `remix` in the Vite plugins list.
|
|
23
|
+
* @experimental
|
|
24
|
+
*/
|
|
25
|
+
declare function oxygen(pluginOptions?: OxygenPluginOptions): Plugin[];
|
|
26
|
+
|
|
27
|
+
export { hydrogen, oxygen };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { setupRemixDevServerHooks, setupHydrogenMiddleware } from './hydrogen-middleware.js';
|
|
3
|
+
import { startMiniOxygenRuntime, setupOxygenMiddleware } from './mini-oxygen.js';
|
|
4
|
+
import { setH2OPluginContext, getH2OPluginContext, DEFAULT_SSR_ENTRY } from './shared.js';
|
|
5
|
+
import { H2O_BINDING_NAME, createLogRequestEvent } from '../request-events.js';
|
|
6
|
+
|
|
7
|
+
function hydrogen(pluginOptions = {}) {
|
|
8
|
+
const isRemixChildCompiler = (config) => !config.plugins?.some((plugin) => plugin.name === "remix");
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
name: "hydrogen:main",
|
|
12
|
+
config(config) {
|
|
13
|
+
return {
|
|
14
|
+
ssr: {
|
|
15
|
+
optimizeDeps: {
|
|
16
|
+
// Add CJS dependencies that break code in workerd
|
|
17
|
+
// with errors like "require/module/exports is not defined":
|
|
18
|
+
include: [
|
|
19
|
+
// React deps:
|
|
20
|
+
"react",
|
|
21
|
+
"react/jsx-runtime",
|
|
22
|
+
"react/jsx-dev-runtime",
|
|
23
|
+
"react-dom",
|
|
24
|
+
"react-dom/server",
|
|
25
|
+
// Remix deps:
|
|
26
|
+
"set-cookie-parser",
|
|
27
|
+
"cookie",
|
|
28
|
+
// Hydrogen deps:
|
|
29
|
+
"content-security-policy-builder"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
// Pass the setup functions to the Oxygen runtime.
|
|
34
|
+
...setH2OPluginContext({
|
|
35
|
+
setupScripts: [setupRemixDevServerHooks],
|
|
36
|
+
shouldStartRuntime: (config2) => !isRemixChildCompiler(config2),
|
|
37
|
+
services: {
|
|
38
|
+
[H2O_BINDING_NAME]: createLogRequestEvent({
|
|
39
|
+
transformLocation: (partialLocation) => path.join(config.root ?? process.cwd(), partialLocation)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
configureServer(viteDevServer) {
|
|
46
|
+
if (isRemixChildCompiler(viteDevServer.config))
|
|
47
|
+
return;
|
|
48
|
+
const { cliOptions } = getH2OPluginContext(viteDevServer.config) || {};
|
|
49
|
+
return () => {
|
|
50
|
+
setupHydrogenMiddleware(viteDevServer, {
|
|
51
|
+
...pluginOptions,
|
|
52
|
+
...cliOptions
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
function oxygen(pluginOptions = {}) {
|
|
60
|
+
let resolvedConfig;
|
|
61
|
+
let absoluteWorkerEntryFile;
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
name: "oxygen:runtime",
|
|
65
|
+
config(config, env) {
|
|
66
|
+
return {
|
|
67
|
+
appType: "custom",
|
|
68
|
+
resolve: {
|
|
69
|
+
conditions: ["worker", "workerd"]
|
|
70
|
+
},
|
|
71
|
+
ssr: {
|
|
72
|
+
noExternal: true,
|
|
73
|
+
target: "webworker"
|
|
74
|
+
},
|
|
75
|
+
// When building, the CLI will set the `ssr` option to `true`
|
|
76
|
+
// if no --entry flag is passed for the default SSR entry file.
|
|
77
|
+
// Replace it here with a default value.
|
|
78
|
+
...env.isSsrBuild && config.build?.ssr && {
|
|
79
|
+
build: {
|
|
80
|
+
ssr: config.build?.ssr === true ? (
|
|
81
|
+
// No --entry flag passed by the user, use the
|
|
82
|
+
// option passed to the plugin or the default value
|
|
83
|
+
pluginOptions.ssrEntry ?? DEFAULT_SSR_ENTRY
|
|
84
|
+
) : (
|
|
85
|
+
// --entry flag passed by the user, keep it
|
|
86
|
+
config.build?.ssr
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
configureServer(viteDevServer) {
|
|
93
|
+
resolvedConfig = viteDevServer.config;
|
|
94
|
+
const { shouldStartRuntime, cliOptions, setupScripts, services } = getH2OPluginContext(resolvedConfig) || {};
|
|
95
|
+
if (shouldStartRuntime && !shouldStartRuntime(resolvedConfig))
|
|
96
|
+
return;
|
|
97
|
+
const workerEntryFile = cliOptions?.ssrEntry ?? pluginOptions.ssrEntry ?? DEFAULT_SSR_ENTRY;
|
|
98
|
+
absoluteWorkerEntryFile = path.isAbsolute(workerEntryFile) ? workerEntryFile : path.resolve(viteDevServer.config.root, workerEntryFile);
|
|
99
|
+
const envPromise = cliOptions?.envPromise ?? Promise.resolve();
|
|
100
|
+
let miniOxygen;
|
|
101
|
+
const miniOxygenPromise = envPromise.then((remoteEnv) => {
|
|
102
|
+
return startMiniOxygenRuntime({
|
|
103
|
+
viteDevServer,
|
|
104
|
+
workerEntryFile,
|
|
105
|
+
setupScripts,
|
|
106
|
+
services,
|
|
107
|
+
env: { ...remoteEnv, ...pluginOptions.env },
|
|
108
|
+
debug: cliOptions?.debug ?? pluginOptions.debug ?? false,
|
|
109
|
+
inspectorPort: cliOptions?.inspectorPort ?? pluginOptions.inspectorPort ?? 9229
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
process.once("SIGTERM", async () => {
|
|
113
|
+
try {
|
|
114
|
+
await miniOxygen?.dispose();
|
|
115
|
+
} finally {
|
|
116
|
+
process.exit();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return () => {
|
|
120
|
+
setupOxygenMiddleware(viteDevServer, async (request) => {
|
|
121
|
+
miniOxygen ??= await miniOxygenPromise;
|
|
122
|
+
return miniOxygen.dispatch(request);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
transform(code, id, options) {
|
|
127
|
+
if (resolvedConfig?.command === "serve" && resolvedConfig?.server?.hmr !== false && options?.ssr && (id === absoluteWorkerEntryFile || id === absoluteWorkerEntryFile + path.extname(id))) {
|
|
128
|
+
return {
|
|
129
|
+
// Accept HMR in server entry module to avoid full-page refresh in the browser.
|
|
130
|
+
// Note: appending code at the end should not break the source map.
|
|
131
|
+
code: code + "\nif (import.meta.hot) import.meta.hot.accept();"
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { hydrogen, oxygen };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const DEFAULT_SSR_ENTRY = "./server";
|
|
2
|
+
const H2O_CONTEXT_KEY = "__h2oPluginContext";
|
|
3
|
+
function getH2OPluginContext(config) {
|
|
4
|
+
return config?.[H2O_CONTEXT_KEY];
|
|
5
|
+
}
|
|
6
|
+
function setH2OPluginContext(options) {
|
|
7
|
+
return { [H2O_CONTEXT_KEY]: options };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { DEFAULT_SSR_ENTRY, getH2OPluginContext, setH2OPluginContext };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { Request } from 'miniflare';
|
|
4
|
+
|
|
5
|
+
function toURL(req = "/", origin) {
|
|
6
|
+
const isRequest = typeof req !== "string";
|
|
7
|
+
const pathname = (isRequest ? req.url : req) || "/";
|
|
8
|
+
return new URL(
|
|
9
|
+
pathname,
|
|
10
|
+
origin || isRequest && req.headers.host && `http://${req.headers.host}` || "http://example.com"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
function toWeb(req, headers) {
|
|
14
|
+
return new Request(toURL(req), {
|
|
15
|
+
method: req.method,
|
|
16
|
+
headers: { ...headers, ...req.headers },
|
|
17
|
+
body: req.headers["content-length"] ? Readable.toWeb(req) : void 0,
|
|
18
|
+
duplex: "half",
|
|
19
|
+
// This is required when sending a ReadableStream as body
|
|
20
|
+
redirect: "manual"
|
|
21
|
+
// Avoid consuming 300 responses here, return to browser
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function pipeFromWeb(webResponse, res) {
|
|
25
|
+
const headers = Object.fromEntries(webResponse.headers.entries());
|
|
26
|
+
const setCookieHeader = "set-cookie";
|
|
27
|
+
if (headers[setCookieHeader]) {
|
|
28
|
+
delete headers[setCookieHeader];
|
|
29
|
+
res.setHeader(setCookieHeader, webResponse.headers.getSetCookie());
|
|
30
|
+
}
|
|
31
|
+
res.writeHead(webResponse.status, webResponse.statusText, headers);
|
|
32
|
+
if (webResponse.body) {
|
|
33
|
+
Readable.fromWeb(webResponse.body).pipe(res);
|
|
34
|
+
} else {
|
|
35
|
+
res.end();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function getHmrUrl(viteDevServer) {
|
|
39
|
+
const userHmrValue = viteDevServer.config.server?.hmr;
|
|
40
|
+
if (userHmrValue === false) {
|
|
41
|
+
console.warn(
|
|
42
|
+
"HMR is disabled. Code changes will not be reflected in neither browser or server."
|
|
43
|
+
);
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
const configHmr = typeof userHmrValue === "object" ? userHmrValue : {};
|
|
47
|
+
const hmrPort = configHmr.port;
|
|
48
|
+
const hmrPath = configHmr.path;
|
|
49
|
+
let hmrBase = viteDevServer.config.base;
|
|
50
|
+
if (hmrPath)
|
|
51
|
+
hmrBase = path.posix.join(hmrBase, hmrPath);
|
|
52
|
+
return (hmrPort ? `http://localhost:${hmrPort}` : "") + hmrBase;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { getHmrUrl, pipeFromWeb, toURL, toWeb };
|