Thing Time 📦

This commit is contained in:
Nikolaj Frey 2023-02-27 19:24:38 +11:00
parent a9f32048f0
commit e8e7321062
18 changed files with 26772 additions and 0 deletions

4
app/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
build
node_modules
bin
*.d.ts

6
app/.eslintrc.js Normal file
View 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
View File

@ -0,0 +1,8 @@
node_modules
/.cache
/build
/dist
/public/build
/.mf
.env

1
app/.graphqlrc.yml Normal file
View File

@ -0,0 +1 @@
schema: node_modules/@shopify/hydrogen-react/storefront.schema.json

2
app/.npmrc Normal file
View File

@ -0,0 +1,2 @@
@shopify:registry=https://registry.npmjs.com
progress=false

42
app/README.md Normal file
View 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

File diff suppressed because it is too large Load Diff

41
app/package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,9 @@
module.exports = {
apps: [
{
script: 'npm run app',
name: "thingtime-app",
namespace: "thingtime"
}
],
};