Skip to content

Commit fdb15b2

Browse files
authored
[hydrogen] Add @vercel/hydrogen Builder (vercel#8071)
Adds a new `@vercel/hydrogen` Builder package so that Vercel can support Shopify Hydrogen projects with zero config. It outputs an Edge Function for the server-side render code and includes a catch-all route to invoke that function after a `handle: "filesystem"` to serve static files that were generated by the build command. **Examples:** * [`hello-world-ts` template](https://hydrogen-hello-world-otm2vmw6w-tootallnate.vercel.app/) * [`demo-store-ts` template](https://hydrogen-demo-store-1gko2fst3-tootallnate.vercel.app/)
1 parent 32ebcd8 commit fdb15b2

File tree

276 files changed

+75735
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

276 files changed

+75735
-0
lines changed

.eslintignore

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ packages/cli/src/util/dev/templates/*.ts
1919
packages/client/tests/fixtures
2020
packages/client/lib
2121

22+
# hydrogen
23+
packages/hydrogen/edge-entry.js
24+
2225
# next
2326
packages/next/test/integration/middleware
2427
packages/next/test/integration/middleware-eval

packages/hydrogen/build.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const execa = require('execa');
2+
const { remove } = require('fs-extra');
3+
4+
async function main() {
5+
await remove('dist');
6+
await execa('tsc', [], { stdio: 'inherit' });
7+
}
8+
9+
main().catch(err => {
10+
console.error(err);
11+
process.exit(1);
12+
});

packages/hydrogen/edge-entry.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import handleRequest from '__RELATIVE__/src/App.server';
2+
import indexTemplate from '__RELATIVE__/dist/client/index.html?raw';
3+
4+
// ReadableStream is bugged in Vercel Edge, overwrite with polyfill
5+
import { ReadableStream } from 'web-streams-polyfill/ponyfill';
6+
Object.assign(globalThis, { ReadableStream });
7+
8+
export default (request, event) =>
9+
handleRequest(request, {
10+
indexTemplate,
11+
context: event,
12+
});

packages/hydrogen/jest.config.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
};

packages/hydrogen/package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@vercel/hydrogen",
3+
"version": "0.0.0",
4+
"license": "MIT",
5+
"main": "./dist/index.js",
6+
"homepage": "https://vercel.com/docs",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/vercel/vercel.git",
10+
"directory": "packages/hydrogen"
11+
},
12+
"scripts": {
13+
"build": "node build.js",
14+
"test-integration-once": "yarn test test/test.js",
15+
"test": "jest --env node --verbose --bail --runInBand",
16+
"prepublishOnly": "node build.js"
17+
},
18+
"files": [
19+
"dist",
20+
"edge-entry.js"
21+
],
22+
"devDependencies": {
23+
"@types/jest": "27.5.1",
24+
"@types/node": "*",
25+
"@vercel/build-utils": "5.0.1-canary.0",
26+
"typescript": "4.6.4"
27+
}
28+
}

packages/hydrogen/src/build.ts

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { promises as fs } from 'fs';
2+
import { dirname, join, relative } from 'path';
3+
import {
4+
debug,
5+
download,
6+
EdgeFunction,
7+
execCommand,
8+
getEnvForPackageManager,
9+
getNodeVersion,
10+
getSpawnOptions,
11+
glob,
12+
readConfigFile,
13+
runNpmInstall,
14+
runPackageJsonScript,
15+
scanParentDirs,
16+
} from '@vercel/build-utils';
17+
import type { BuildV2, PackageJson } from '@vercel/build-utils';
18+
19+
export const build: BuildV2 = async ({
20+
entrypoint,
21+
files,
22+
workPath,
23+
config,
24+
meta = {},
25+
}) => {
26+
const { installCommand, buildCommand } = config;
27+
28+
await download(files, workPath, meta);
29+
30+
const mountpoint = dirname(entrypoint);
31+
const entrypointDir = join(workPath, mountpoint);
32+
33+
// Run "Install Command"
34+
const nodeVersion = await getNodeVersion(
35+
entrypointDir,
36+
undefined,
37+
config,
38+
meta
39+
);
40+
41+
const spawnOpts = getSpawnOptions(meta, nodeVersion);
42+
const { cliType, lockfileVersion } = await scanParentDirs(entrypointDir);
43+
44+
spawnOpts.env = getEnvForPackageManager({
45+
cliType,
46+
lockfileVersion,
47+
nodeVersion,
48+
env: spawnOpts.env || {},
49+
});
50+
51+
if (typeof installCommand === 'string') {
52+
if (installCommand.trim()) {
53+
console.log(`Running "install" command: \`${installCommand}\`...`);
54+
await execCommand(installCommand, {
55+
...spawnOpts,
56+
cwd: entrypointDir,
57+
});
58+
} else {
59+
console.log(`Skipping "install" command...`);
60+
}
61+
} else {
62+
await runNpmInstall(entrypointDir, [], spawnOpts, meta, nodeVersion);
63+
}
64+
65+
// Copy the edge entrypoint file into `.vercel/cache`
66+
const edgeEntryDir = join(workPath, '.vercel/cache/hydrogen');
67+
const edgeEntryRelative = relative(edgeEntryDir, workPath);
68+
const edgeEntryDest = join(edgeEntryDir, 'edge-entry.js');
69+
let edgeEntryContents = await fs.readFile(
70+
join(__dirname, '..', 'edge-entry.js'),
71+
'utf8'
72+
);
73+
edgeEntryContents = edgeEntryContents.replace(
74+
/__RELATIVE__/g,
75+
edgeEntryRelative
76+
);
77+
await fs.mkdir(edgeEntryDir, { recursive: true });
78+
await fs.writeFile(edgeEntryDest, edgeEntryContents);
79+
80+
// Make `shopify hydrogen build` output a Edge Function compatible bundle
81+
spawnOpts.env.SHOPIFY_FLAG_BUILD_TARGET = 'worker';
82+
83+
// Use this file as the entrypoint for the Edge Function bundle build
84+
spawnOpts.env.SHOPIFY_FLAG_BUILD_SSR_ENTRY = edgeEntryDest;
85+
86+
// Run "Build Command"
87+
if (buildCommand) {
88+
debug(`Executing build command "${buildCommand}"`);
89+
await execCommand(buildCommand, {
90+
...spawnOpts,
91+
cwd: entrypointDir,
92+
});
93+
} else {
94+
const pkg = await readConfigFile<PackageJson>(
95+
join(entrypointDir, 'package.json')
96+
);
97+
if (hasScript('vercel-build', pkg)) {
98+
debug(`Executing "yarn vercel-build"`);
99+
await runPackageJsonScript(entrypointDir, 'vercel-build', spawnOpts);
100+
} else if (hasScript('build', pkg)) {
101+
debug(`Executing "yarn build"`);
102+
await runPackageJsonScript(entrypointDir, 'build', spawnOpts);
103+
} else {
104+
await execCommand('shopify hydrogen build', {
105+
...spawnOpts,
106+
cwd: entrypointDir,
107+
});
108+
}
109+
}
110+
111+
const [staticFiles, edgeFunctionFiles] = await Promise.all([
112+
glob('**', join(entrypointDir, 'dist/client')),
113+
glob('**', join(entrypointDir, 'dist/worker')),
114+
]);
115+
116+
const edgeFunction = new EdgeFunction({
117+
name: 'hydrogen',
118+
deploymentTarget: 'v8-worker',
119+
entrypoint: 'index.js',
120+
files: edgeFunctionFiles,
121+
});
122+
123+
// The `index.html` file is a template, but we want to serve the
124+
// SSR version instead, so omit this static file from the output
125+
delete staticFiles['index.html'];
126+
127+
return {
128+
routes: [
129+
{
130+
handle: 'filesystem',
131+
},
132+
{
133+
src: '/(.*)',
134+
dest: '/hydrogen',
135+
},
136+
],
137+
output: {
138+
hydrogen: edgeFunction,
139+
...staticFiles,
140+
},
141+
};
142+
};
143+
144+
function hasScript(scriptName: string, pkg: PackageJson | null) {
145+
const scripts = pkg?.scripts || {};
146+
return typeof scripts[scriptName] === 'string';
147+
}

packages/hydrogen/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const version = 2;
2+
export * from './build';
3+
export * from './prepare-cache';
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { glob } from '@vercel/build-utils';
2+
import type { PrepareCache } from '@vercel/build-utils';
3+
4+
export const prepareCache: PrepareCache = ({ repoRootPath, workPath }) => {
5+
return glob('**/node_modules/**', repoRootPath || workPath);
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "Shopify Hydrogen",
3+
"image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:0-16",
4+
"settings": {},
5+
"extensions": [
6+
"graphql.vscode-graphql",
7+
"dbaeumer.vscode-eslint",
8+
"bradlc.vscode-tailwindcss",
9+
"esbenp.prettier-vscode"
10+
],
11+
"forwardPorts": [3000],
12+
"postCreateCommand": "yarn install",
13+
"postStartCommand": "yarn dev",
14+
"remoteUser": "node",
15+
"features": {
16+
"git": "latest"
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# THIS IS A STUB FOR NEW HYDROGEN APPS
2+
# THIS WILL EVENTUALLY MOVE TO A /TEMPLATE-* FOLDER
3+
4+
# Logs
5+
logs
6+
*.log
7+
npm-debug.log*
8+
yarn-debug.log*
9+
yarn-error.log*
10+
lerna-debug.log*
11+
12+
# Diagnostic reports (https://nodejs.org/api/report.html)
13+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14+
15+
# Runtime data
16+
pids
17+
*.pid
18+
*.seed
19+
*.pid.lock
20+
21+
# Directory for instrumented libs generated by jscoverage/JSCover
22+
lib-cov
23+
24+
# Coverage directory used by tools like istanbul
25+
coverage
26+
*.lcov
27+
28+
# nyc test coverage
29+
.nyc_output
30+
31+
# Compiled binary addons (https://nodejs.org/api/addons.html)
32+
build/Release
33+
34+
# Dependency directories
35+
node_modules/
36+
jspm_packages/
37+
38+
# TypeScript cache
39+
*.tsbuildinfo
40+
41+
# Optional npm cache directory
42+
.npm
43+
44+
# Optional eslint cache
45+
.eslintcache
46+
47+
# Microbundle cache
48+
.rpt2_cache/
49+
.rts2_cache_cjs/
50+
.rts2_cache_es/
51+
.rts2_cache_umd/
52+
53+
# Optional REPL history
54+
.node_repl_history
55+
56+
# Output of 'npm pack'
57+
*.tgz
58+
59+
# Yarn Integrity file
60+
.yarn-integrity
61+
62+
# dotenv environment variables file
63+
.env
64+
.env.test
65+
66+
# Serverless directories
67+
.serverless/
68+
69+
# Stores VSCode versions used for testing VSCode extensions
70+
.vscode-test
71+
72+
# yarn v2
73+
.yarn/cache
74+
.yarn/unplugged
75+
.yarn/build-state.yml
76+
.yarn/install-state.gz
77+
.pnp.*
78+
79+
# Vite output
80+
dist
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Hydrogen Demo Store
2+
3+
Hydrogen is a React framework and SDK that you can use to build fast and dynamic Shopify custom storefronts.
4+
5+
[Check out the docs](https://shopify.dev/custom-storefronts/hydrogen)
6+
7+
[Run this template on StackBlitz](https://stackblitz.com/github/Shopify/hydrogen/tree/stackblitz/templates/demo-store)
8+
9+
## Getting started
10+
11+
**Requirements:**
12+
13+
- Node.js version 16.5.0 or higher
14+
- Yarn
15+
16+
To create a new Hydrogen app, run:
17+
18+
```bash
19+
npm init @shopify/hydrogen
20+
```
21+
22+
## Running the dev server
23+
24+
Then `cd` into the new directory and run:
25+
26+
```bash
27+
npm install
28+
npm run dev
29+
```
30+
31+
Remember to update `hydrogen.config.js` with your shop's domain and Storefront API token!
32+
33+
## Building for production
34+
35+
```bash
36+
npm run build
37+
```
38+
39+
## Previewing a production build
40+
41+
To run a local preview of your Hydrogen app in an environment similar to Oxygen, build your Hydrogen app and then run `npm run preview`:
42+
43+
```bash
44+
npm run build
45+
npm run preview
46+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {defineConfig, CookieSessionStorage} from '@shopify/hydrogen/config';
2+
3+
export default defineConfig({
4+
shopify: {
5+
defaultCountryCode: 'US',
6+
defaultLanguageCode: 'EN',
7+
storeDomain: 'hydrogen-preview.myshopify.com',
8+
storefrontToken: '3b580e70970c4528da70c98e097c2fa0',
9+
storefrontApiVersion: '2022-07',
10+
},
11+
session: CookieSessionStorage('__session', {
12+
path: '/',
13+
httpOnly: true,
14+
secure: import.meta.env.PROD,
15+
sameSite: 'Strict',
16+
maxAge: 60 * 60 * 24 * 30,
17+
}),
18+
});

0 commit comments

Comments
 (0)