Thing Time 📦
This commit is contained in:
parent
a9f32048f0
commit
e8e7321062
4
app/.eslintignore
Normal file
4
app/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
build
|
||||||
|
node_modules
|
||||||
|
bin
|
||||||
|
*.d.ts
|
6
app/.eslintrc.js
Normal file
6
app/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @type {import("@types/eslint").Linter.BaseConfig}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
extends: ['plugin:hydrogen/recommended', 'plugin:hydrogen/typescript'],
|
||||||
|
};
|
8
app/.gitignore
vendored
Normal file
8
app/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
/.cache
|
||||||
|
/build
|
||||||
|
/dist
|
||||||
|
/public/build
|
||||||
|
/.mf
|
||||||
|
.env
|
1
app/.graphqlrc.yml
Normal file
1
app/.graphqlrc.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
schema: node_modules/@shopify/hydrogen-react/storefront.schema.json
|
2
app/.npmrc
Normal file
2
app/.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@shopify:registry=https://registry.npmjs.com
|
||||||
|
progress=false
|
42
app/README.md
Normal file
42
app/README.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Hydrogen template: Hello World
|
||||||
|
|
||||||
|
Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen.
|
||||||
|
|
||||||
|
[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
|
||||||
|
[Get familiar with Remix](https://remix.run/docs/en/v1)
|
||||||
|
|
||||||
|
## What's included
|
||||||
|
|
||||||
|
- Remix
|
||||||
|
- Hydrogen
|
||||||
|
- Oxygen
|
||||||
|
- Shopify CLI
|
||||||
|
- ESLint
|
||||||
|
- Prettier
|
||||||
|
- GraphQL generator
|
||||||
|
- TypeScript and JavaScript flavors
|
||||||
|
- Minimal setup of components and routes
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
|
||||||
|
- Node.js version 16.14.0 or higher
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create @shopify/hydrogen@latest --template hello-world
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to update `.env` with your shop's domain and Storefront API token!
|
||||||
|
|
||||||
|
## Building for production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
26292
app/package-lock.json
generated
Normal file
26292
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
app/package.json
Normal file
41
app/package.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "hello-world",
|
||||||
|
"private": true,
|
||||||
|
"sideEffects": false,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "shopify hydrogen build",
|
||||||
|
"dev": "shopify hydrogen dev",
|
||||||
|
"preview": "npm run build && shopify hydrogen preview",
|
||||||
|
"lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx .",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"g": "shopify hydrogen generate"
|
||||||
|
},
|
||||||
|
"prettier": "@shopify/prettier-config",
|
||||||
|
"dependencies": {
|
||||||
|
"@remix-run/react": "1.12.0",
|
||||||
|
"@shopify/cli": "3.29.0",
|
||||||
|
"@shopify/cli-hydrogen": "^4.0.8",
|
||||||
|
"@shopify/hydrogen": "^2023.1.5",
|
||||||
|
"@shopify/remix-oxygen": "^1.0.3",
|
||||||
|
"graphql": "^16.6.0",
|
||||||
|
"graphql-tag": "^2.12.6",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@remix-run/dev": "1.12.0",
|
||||||
|
"@shopify/oxygen-workers-types": "^3.17.2",
|
||||||
|
"@shopify/prettier-config": "^1.1.2",
|
||||||
|
"@types/eslint": "^8.4.10",
|
||||||
|
"@types/react": "^18.0.20",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"eslint": "^8.20.0",
|
||||||
|
"eslint-plugin-hydrogen": "0.12.2",
|
||||||
|
"prettier": "^2.8.4",
|
||||||
|
"typescript": "^4.9.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.13"
|
||||||
|
}
|
||||||
|
}
|
28
app/public/favicon.svg
Normal file
28
app/public/favicon.svg
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
|
||||||
|
<style>
|
||||||
|
.stroke {
|
||||||
|
stroke: #000;
|
||||||
|
}
|
||||||
|
.fill {
|
||||||
|
fill: #000;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.stroke {
|
||||||
|
stroke: #fff;
|
||||||
|
}
|
||||||
|
.fill {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path
|
||||||
|
class="stroke"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.1 16.04 1 8.02 6.16 5.3l5.82 3.09 4.88-2.57-5.82-3.1L16.21 0l15.1 8.02-5.17 2.72-5.5-2.91-4.88 2.57 5.5 2.92-5.16 2.72Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="fill"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.1 32 1 23.98l5.16-2.72 5.82 3.08 4.88-2.57-5.82-3.08 5.17-2.73 15.1 8.02-5.17 2.72-5.5-2.92-4.88 2.58 5.5 2.92L16.1 32Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 690 B |
19
app/remix.config.js
Normal file
19
app/remix.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/** @type {import('@remix-run/dev').AppConfig} */
|
||||||
|
module.exports = {
|
||||||
|
appDirectory: 'src',
|
||||||
|
ignoredRouteFiles: ['**/.*'],
|
||||||
|
watchPaths: ['./public'],
|
||||||
|
server: './server.ts',
|
||||||
|
/**
|
||||||
|
* The following settings are required to deploy Hydrogen apps to Oxygen:
|
||||||
|
*/
|
||||||
|
publicPath: (process.env.HYDROGEN_ASSET_BASE_URL ?? '/') + 'build/',
|
||||||
|
assetsBuildDirectory: 'dist/client/build',
|
||||||
|
serverBuildPath: 'dist/worker/index.js',
|
||||||
|
serverMainFields: ['browser', 'module', 'main'],
|
||||||
|
serverConditions: ['worker', process.env.NODE_ENV],
|
||||||
|
serverDependenciesToBundle: 'all',
|
||||||
|
serverModuleFormat: 'esm',
|
||||||
|
serverPlatform: 'neutral',
|
||||||
|
serverMinify: process.env.NODE_ENV === 'production',
|
||||||
|
};
|
36
app/remix.env.d.ts
vendored
Normal file
36
app/remix.env.d.ts
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/// <reference types="@remix-run/dev" />
|
||||||
|
/// <reference types="@shopify/remix-oxygen" />
|
||||||
|
/// <reference types="@shopify/oxygen-workers-types" />
|
||||||
|
|
||||||
|
import type {Storefront} from '@shopify/hydrogen';
|
||||||
|
import type {HydrogenSession} from '../server';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
/**
|
||||||
|
* A global `process` object is only available during build to access NODE_ENV.
|
||||||
|
*/
|
||||||
|
const process: {env: {NODE_ENV: 'production' | 'development'}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare expected Env parameter in fetch handler.
|
||||||
|
*/
|
||||||
|
interface Env {
|
||||||
|
SESSION_SECRET: string;
|
||||||
|
PUBLIC_STOREFRONT_API_TOKEN: string;
|
||||||
|
PRIVATE_STOREFRONT_API_TOKEN: string;
|
||||||
|
PUBLIC_STOREFRONT_API_VERSION: string;
|
||||||
|
PUBLIC_STORE_DOMAIN: string;
|
||||||
|
PUBLIC_STOREFRONT_ID: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare local additions to `AppLoadContext` to include the session utilities we injected in `server.ts`.
|
||||||
|
*/
|
||||||
|
declare module '@shopify/remix-oxygen' {
|
||||||
|
export interface AppLoadContext {
|
||||||
|
session: HydrogenSession;
|
||||||
|
storefront: Storefront;
|
||||||
|
env: Env;
|
||||||
|
}
|
||||||
|
}
|
131
app/server.ts
Normal file
131
app/server.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Virtual entry point for the app
|
||||||
|
import * as remixBuild from '@remix-run/dev/server-build';
|
||||||
|
import {createStorefrontClient, storefrontRedirect} from '@shopify/hydrogen';
|
||||||
|
import {
|
||||||
|
createRequestHandler,
|
||||||
|
getBuyerIp,
|
||||||
|
createCookieSessionStorage,
|
||||||
|
type SessionStorage,
|
||||||
|
type Session,
|
||||||
|
} from '@shopify/remix-oxygen';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a fetch handler in module format.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
async fetch(
|
||||||
|
request: Request,
|
||||||
|
env: Env,
|
||||||
|
executionContext: ExecutionContext,
|
||||||
|
): Promise<Response> {
|
||||||
|
try {
|
||||||
|
/**
|
||||||
|
* Open a cache instance in the worker and a custom session instance.
|
||||||
|
*/
|
||||||
|
if (!env?.SESSION_SECRET) {
|
||||||
|
throw new Error('SESSION_SECRET environment variable is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitUntil = (p: Promise<any>) => executionContext.waitUntil(p);
|
||||||
|
const [cache, session] = await Promise.all([
|
||||||
|
caches.open('hydrogen'),
|
||||||
|
HydrogenSession.init(request, [env.SESSION_SECRET]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Hydrogen's Storefront client.
|
||||||
|
*/
|
||||||
|
const {storefront} = createStorefrontClient({
|
||||||
|
cache,
|
||||||
|
waitUntil,
|
||||||
|
buyerIp: getBuyerIp(request),
|
||||||
|
i18n: {language: 'EN', country: 'US'},
|
||||||
|
publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
|
||||||
|
privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
|
||||||
|
storeDomain: `https://${env.PUBLIC_STORE_DOMAIN}`,
|
||||||
|
storefrontApiVersion: env.PUBLIC_STOREFRONT_API_VERSION || '2023-01',
|
||||||
|
storefrontId: env.PUBLIC_STOREFRONT_ID,
|
||||||
|
requestGroupId: request.headers.get('request-id'),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Remix request handler and pass
|
||||||
|
* Hydrogen's Storefront client to the loader context.
|
||||||
|
*/
|
||||||
|
const handleRequest = createRequestHandler({
|
||||||
|
build: remixBuild,
|
||||||
|
mode: process.env.NODE_ENV,
|
||||||
|
getLoadContext: () => ({session, storefront, env}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await handleRequest(request);
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
/**
|
||||||
|
* Check for redirects only when there's a 404 from the app.
|
||||||
|
* If the redirect doesn't exist, then `storefrontRedirect`
|
||||||
|
* will pass through the 404 response.
|
||||||
|
*/
|
||||||
|
return storefrontRedirect({request, response, storefront});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error);
|
||||||
|
return new Response('An unexpected error occurred', {status: 500});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a custom session implementation for your Hydrogen shop.
|
||||||
|
* Feel free to customize it to your needs, add helper methods, or
|
||||||
|
* swap out the cookie-based implementation with something else!
|
||||||
|
*/
|
||||||
|
class HydrogenSession {
|
||||||
|
constructor(
|
||||||
|
private sessionStorage: SessionStorage,
|
||||||
|
private session: Session,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static async init(request: Request, secrets: string[]) {
|
||||||
|
const storage = createCookieSessionStorage({
|
||||||
|
cookie: {
|
||||||
|
name: 'session',
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'lax',
|
||||||
|
secrets,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await storage.getSession(request.headers.get('Cookie'));
|
||||||
|
|
||||||
|
return new this(storage, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string) {
|
||||||
|
return this.session.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
return this.sessionStorage.destroySession(this.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
flash(key: string, value: any) {
|
||||||
|
this.session.flash(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
unset(key: string) {
|
||||||
|
this.session.unset(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: any) {
|
||||||
|
this.session.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
commit() {
|
||||||
|
return this.sessionStorage.commitSession(this.session);
|
||||||
|
}
|
||||||
|
}
|
4
app/src/entry.client.tsx
Normal file
4
app/src/entry.client.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import {RemixBrowser} from '@remix-run/react';
|
||||||
|
import {hydrateRoot} from 'react-dom/client';
|
||||||
|
|
||||||
|
hydrateRoot(document, <RemixBrowser />);
|
21
app/src/entry.server.tsx
Normal file
21
app/src/entry.server.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type {EntryContext} from '@shopify/remix-oxygen';
|
||||||
|
import {RemixServer} from '@remix-run/react';
|
||||||
|
import {renderToReadableStream} from 'react-dom/server';
|
||||||
|
|
||||||
|
export default async function handleRequest(
|
||||||
|
request: Request,
|
||||||
|
responseStatusCode: number,
|
||||||
|
responseHeaders: Headers,
|
||||||
|
remixContext: EntryContext,
|
||||||
|
) {
|
||||||
|
const body = await renderToReadableStream(
|
||||||
|
<RemixServer context={remixContext} url={request.url} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
responseHeaders.set('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
return new Response(body, {
|
||||||
|
status: responseStatusCode,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
}
|
72
app/src/root.tsx
Normal file
72
app/src/root.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
type LinksFunction,
|
||||||
|
type MetaFunction,
|
||||||
|
type LoaderArgs,
|
||||||
|
} from '@shopify/remix-oxygen';
|
||||||
|
import {
|
||||||
|
Links,
|
||||||
|
Meta,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
ScrollRestoration,
|
||||||
|
useLoaderData,
|
||||||
|
} from '@remix-run/react';
|
||||||
|
import type {Shop} from '@shopify/hydrogen/storefront-api-types';
|
||||||
|
import styles from './styles/app.css';
|
||||||
|
import favicon from '../public/favicon.svg';
|
||||||
|
|
||||||
|
export const links: LinksFunction = () => {
|
||||||
|
return [
|
||||||
|
{rel: 'stylesheet', href: styles},
|
||||||
|
{
|
||||||
|
rel: 'preconnect',
|
||||||
|
href: 'https://cdn.shopify.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'preconnect',
|
||||||
|
href: 'https://shop.app',
|
||||||
|
},
|
||||||
|
{rel: 'icon', type: 'image/svg+xml', href: favicon},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const meta: MetaFunction = () => ({
|
||||||
|
charset: 'utf-8',
|
||||||
|
viewport: 'width=device-width,initial-scale=1',
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function loader({context}: LoaderArgs) {
|
||||||
|
const layout = await context.storefront.query<{shop: Shop}>(LAYOUT_QUERY);
|
||||||
|
return {layout};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const data = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
const {name} = data.layout.shop;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello, {name}</h1>
|
||||||
|
<p>This is a custom storefront powered by Hydrogen</p>
|
||||||
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAYOUT_QUERY = `#graphql
|
||||||
|
query layout {
|
||||||
|
shop {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
31
app/src/styles/app.css
Normal file
31
app/src/styles/app.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #FFFFFF;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
25
app/tsconfig.json
Normal file
25
app/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"strict": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"types": ["@shopify/oxygen-workers-types"],
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remix takes care of building everything in `./app` with `remix build`.
|
||||||
|
// Wrangler takes care of building everything in `./worker` with `wrangler start` / `wrangler publish`.
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
9
ecosystem.config.js
Normal file
9
ecosystem.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
script: 'npm run app',
|
||||||
|
name: "thingtime-app",
|
||||||
|
namespace: "thingtime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user