@pack/hydrogen 0.0.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/README.md +1 -0
- package/dist/create-pack-client.d.ts +52 -0
- package/dist/create-pack-client.d.ts.map +1 -0
- package/dist/create-pack-client.js +61 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/preview/preview-mode.d.ts +11 -0
- package/dist/preview/preview-mode.d.ts.map +1 -0
- package/dist/preview/preview-mode.js +73 -0
- package/dist/preview/preview-session.d.ts +12 -0
- package/dist/preview/preview-session.d.ts.map +1 -0
- package/dist/preview/preview-session.js +36 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# hydrogen
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/// <reference types="@shopify/oxygen-workers-types" />
|
|
2
|
+
import { PackClient } from '@pack/client';
|
|
3
|
+
import { CacheLong } from '@shopify/hydrogen';
|
|
4
|
+
import { PreviewSession } from './preview/preview-session';
|
|
5
|
+
/** @see https://shopify.dev/docs/custom-storefronts/hydrogen/data-fetching/cache#caching-strategies */
|
|
6
|
+
type CachingStrategy = ReturnType<typeof CacheLong>;
|
|
7
|
+
interface EnvironmentOptions {
|
|
8
|
+
/**
|
|
9
|
+
* A Cache API instance.
|
|
10
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Cache
|
|
11
|
+
*/
|
|
12
|
+
cache: Cache;
|
|
13
|
+
/**
|
|
14
|
+
* A runtime utility for serverless environments
|
|
15
|
+
* @see https://developers.cloudflare.com/workers/runtime-apis/fetch-event/#waituntil
|
|
16
|
+
*/
|
|
17
|
+
waitUntil: ExecutionContext['waitUntil'];
|
|
18
|
+
}
|
|
19
|
+
interface CreatePackClientOptions extends EnvironmentOptions {
|
|
20
|
+
apiUrl?: string;
|
|
21
|
+
token: string;
|
|
22
|
+
preview?: {
|
|
23
|
+
session: PreviewSession;
|
|
24
|
+
};
|
|
25
|
+
contentEnvironment?: string;
|
|
26
|
+
}
|
|
27
|
+
type Variables = Record<string, any>;
|
|
28
|
+
interface QueryOptions {
|
|
29
|
+
variables?: Variables;
|
|
30
|
+
cache?: CachingStrategy;
|
|
31
|
+
}
|
|
32
|
+
interface QueryError {
|
|
33
|
+
message: string;
|
|
34
|
+
param?: string;
|
|
35
|
+
code?: string;
|
|
36
|
+
type: string;
|
|
37
|
+
}
|
|
38
|
+
interface QueryResponse<T> {
|
|
39
|
+
data: T | null;
|
|
40
|
+
error: QueryError | null;
|
|
41
|
+
}
|
|
42
|
+
export interface Pack {
|
|
43
|
+
isPreviewModeEnabled: () => boolean;
|
|
44
|
+
preview?: {
|
|
45
|
+
session: PreviewSession;
|
|
46
|
+
};
|
|
47
|
+
query: <T = any>(query: string, options?: QueryOptions) => Promise<QueryResponse<T>>;
|
|
48
|
+
isValidEditToken: PackClient['isValidEditToken'];
|
|
49
|
+
}
|
|
50
|
+
export declare function createPackClient(options: CreatePackClientOptions): Pack;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=create-pack-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-pack-client.d.ts","sourceRoot":"","sources":["../src/create-pack-client.ts"],"names":[],"mappings":";AAAA,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAA;AACvC,OAAO,EAAC,SAAS,EAAkB,MAAM,mBAAmB,CAAA;AAE5D,OAAO,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAA;AAExD,uGAAuG;AACvG,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAA;AAEnD,UAAU,kBAAkB;IAC1B;;;OAGG;IACH,KAAK,EAAE,KAAK,CAAA;IACZ;;;OAGG;IACH,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;CACzC;AAED,UAAU,uBAAwB,SAAQ,kBAAkB;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE;QACR,OAAO,EAAE,cAAc,CAAA;KACxB,CAAA;IACD,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAEpC,UAAU,YAAY;IACpB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,KAAK,CAAC,EAAE,eAAe,CAAA;CACxB;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED,UAAU,aAAa,CAAC,CAAC;IACvB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;IACd,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,IAAI;IACnB,oBAAoB,EAAE,MAAM,OAAO,CAAA;IACnC,OAAO,CAAC,EAAE;QACR,OAAO,EAAE,cAAc,CAAA;KACxB,CAAA;IACD,KAAK,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,gBAAgB,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAA;CACjD;AA8BD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,IAAI,CA2CvE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { PackClient } from '@pack/client';
|
|
2
|
+
import { CacheLong, createWithCache } from '@shopify/hydrogen';
|
|
3
|
+
const PRODUCTION_ENVIRONMENT = 'production';
|
|
4
|
+
/**
|
|
5
|
+
* Create an SHA-256 hash as a hex string
|
|
6
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
|
|
7
|
+
*/
|
|
8
|
+
async function sha256(message) {
|
|
9
|
+
// encode as UTF-8
|
|
10
|
+
const messageBuffer = new TextEncoder().encode(message);
|
|
11
|
+
// hash the message
|
|
12
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer);
|
|
13
|
+
// convert bytes to hex string
|
|
14
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
15
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
16
|
+
.join('');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Hash query and its parameters for use as cache key
|
|
20
|
+
* NOTE: Oxygen deployment will break if the cache key is long or contains `\n`
|
|
21
|
+
*/
|
|
22
|
+
function hashQuery(query, variables) {
|
|
23
|
+
let hash = query;
|
|
24
|
+
if (variables !== null)
|
|
25
|
+
hash += JSON.stringify(variables);
|
|
26
|
+
return sha256(hash);
|
|
27
|
+
}
|
|
28
|
+
export function createPackClient(options) {
|
|
29
|
+
const { cache, waitUntil, preview, contentEnvironment, token, apiUrl } = options;
|
|
30
|
+
const previewEnabled = !!preview?.session.get('enabled');
|
|
31
|
+
const previewEnvironment = preview?.session.get('environment');
|
|
32
|
+
const clientContentEnvironment = previewEnvironment || contentEnvironment || PRODUCTION_ENVIRONMENT;
|
|
33
|
+
const packClient = new PackClient({
|
|
34
|
+
apiUrl,
|
|
35
|
+
token,
|
|
36
|
+
contentEnvironment: clientContentEnvironment,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
preview,
|
|
40
|
+
isPreviewModeEnabled: () => previewEnabled,
|
|
41
|
+
async query(query, { variables, cache: strategy = CacheLong() } = {}) {
|
|
42
|
+
const queryHash = await hashQuery(query, variables);
|
|
43
|
+
const withCache = createWithCache({
|
|
44
|
+
cache,
|
|
45
|
+
waitUntil,
|
|
46
|
+
});
|
|
47
|
+
const queryVariables = variables ? { ...variables } : {};
|
|
48
|
+
if (previewEnabled) {
|
|
49
|
+
queryVariables.version = 'CURRENT';
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
queryVariables.version = 'PUBLISHED';
|
|
53
|
+
}
|
|
54
|
+
// Preview mode always bypasses the cache
|
|
55
|
+
if (previewEnabled)
|
|
56
|
+
return packClient.fetch(query, { variables: queryVariables });
|
|
57
|
+
return withCache(queryHash, strategy, () => packClient.fetch(query, { variables: queryVariables }));
|
|
58
|
+
},
|
|
59
|
+
isValidEditToken: (token) => packClient.isValidEditToken(token),
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Pack, createPackClient } from "./create-pack-client";
|
|
2
|
+
import { PreviewSession } from "./preview/preview-session";
|
|
3
|
+
import { action as previewModeAction, loader as previewModeLoader } from "./preview/preview-mode";
|
|
4
|
+
export { type Pack, createPackClient, PreviewSession, previewModeAction, previewModeLoader, };
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EACL,MAAM,IAAI,iBAAiB,EAC3B,MAAM,IAAI,iBAAiB,EAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,KAAK,IAAI,EACT,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,iBAAiB,GAClB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createPackClient } from "./create-pack-client";
|
|
2
|
+
import { PreviewSession } from "./preview/preview-session";
|
|
3
|
+
import { action as previewModeAction, loader as previewModeLoader, } from "./preview/preview-mode";
|
|
4
|
+
export { createPackClient, PreviewSession, previewModeAction, previewModeLoader, };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ActionFunction, type LoaderFunction } from "@shopify/remix-oxygen";
|
|
2
|
+
/**
|
|
3
|
+
* A `POST` request to this route will exit preview mode
|
|
4
|
+
* POST /api/edit Content-Type: application/x-www-form-urlencoded
|
|
5
|
+
*/
|
|
6
|
+
export declare const action: ActionFunction;
|
|
7
|
+
/**
|
|
8
|
+
* A `GET` request to this route will enter preview mode
|
|
9
|
+
*/
|
|
10
|
+
export declare const loader: LoaderFunction;
|
|
11
|
+
//# sourceMappingURL=preview-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-mode.d.ts","sourceRoot":"","sources":["../../src/preview/preview-mode.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,cAAc,EAEpB,MAAM,uBAAuB,CAAC;AAsB/B;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,cAkBpB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,MAAM,EAAE,cA+BpB,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { json, redirect, } from "@shopify/remix-oxygen";
|
|
2
|
+
const ROOT_PATH = "/";
|
|
3
|
+
/**
|
|
4
|
+
* A not found response. Sets the status code.
|
|
5
|
+
*/
|
|
6
|
+
function notFound(message = "Not Found") {
|
|
7
|
+
return new Response(message, { status: 404, statusText: "Not Found" });
|
|
8
|
+
}
|
|
9
|
+
function isLocalPath(request, url) {
|
|
10
|
+
// Our domain, based on the current request path
|
|
11
|
+
const currentUrl = new URL(request.url);
|
|
12
|
+
// If url is relative, the 2nd argument will act as the base domain.
|
|
13
|
+
const urlToCheck = new URL(url, currentUrl.origin);
|
|
14
|
+
// If the origins don't match the slug is not on our domain.
|
|
15
|
+
return currentUrl.origin === urlToCheck.origin;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A `POST` request to this route will exit preview mode
|
|
19
|
+
* POST /api/edit Content-Type: application/x-www-form-urlencoded
|
|
20
|
+
*/
|
|
21
|
+
export const action = async ({ request, context }) => {
|
|
22
|
+
const { preview } = context.pack;
|
|
23
|
+
if (!(request.method === "POST" && preview?.session)) {
|
|
24
|
+
return json({ message: "Method not allowed" }, 405);
|
|
25
|
+
}
|
|
26
|
+
const body = await request.formData();
|
|
27
|
+
const slug = body.get("slug") ?? ROOT_PATH;
|
|
28
|
+
const redirectTo = isLocalPath(request, slug) ? slug : ROOT_PATH;
|
|
29
|
+
preview.session.set("enabled", false);
|
|
30
|
+
return redirect(redirectTo, {
|
|
31
|
+
headers: {
|
|
32
|
+
"Set-Cookie": await preview.session.destroy(),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A `GET` request to this route will enter preview mode
|
|
38
|
+
*/
|
|
39
|
+
export const loader = async function ({ request, context }) {
|
|
40
|
+
const { pack } = context;
|
|
41
|
+
if (!pack.preview?.session)
|
|
42
|
+
return notFound();
|
|
43
|
+
const { searchParams } = new URL(request.url);
|
|
44
|
+
const token = searchParams.get("token");
|
|
45
|
+
const environment = searchParams.get("environment");
|
|
46
|
+
const path = searchParams.get("path") ?? ROOT_PATH;
|
|
47
|
+
const redirectTo = isLocalPath(request, path) ? path : ROOT_PATH;
|
|
48
|
+
if (!searchParams.has("token")) {
|
|
49
|
+
throw new MissingTokenError();
|
|
50
|
+
}
|
|
51
|
+
const isValidToken = await pack.isValidEditToken(token);
|
|
52
|
+
if (!isValidToken) {
|
|
53
|
+
throw new InvalidTokenError();
|
|
54
|
+
}
|
|
55
|
+
pack.preview.session.set("enabled", true);
|
|
56
|
+
pack.preview.session.set("environment", environment);
|
|
57
|
+
return redirect(redirectTo, {
|
|
58
|
+
status: 307,
|
|
59
|
+
headers: {
|
|
60
|
+
"Set-Cookie": await pack.preview.session.commit(),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
class MissingTokenError extends Response {
|
|
65
|
+
constructor() {
|
|
66
|
+
super("Missing token", { status: 401, statusText: "Unauthorized" });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class InvalidTokenError extends Response {
|
|
70
|
+
constructor() {
|
|
71
|
+
super("Invalid token", { status: 401, statusText: "Unauthorized" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Session, type SessionStorage } from '@shopify/remix-oxygen';
|
|
2
|
+
export declare class PreviewSession {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(sessionStorage: SessionStorage, session: Session);
|
|
5
|
+
static init(request: Request, secrets: string[]): Promise<PreviewSession>;
|
|
6
|
+
has(key: string): boolean;
|
|
7
|
+
get(key: string): any;
|
|
8
|
+
destroy(): Promise<string>;
|
|
9
|
+
set(key: string, value: any): void;
|
|
10
|
+
commit(): Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=preview-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-session.d.ts","sourceRoot":"","sources":["../../src/preview/preview-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,OAAO,EACZ,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAA;AAE9B,qBAAa,cAAc;;gBAIb,cAAc,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO;WAK/C,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;IAe/E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,GAAG,CAAC,GAAG,EAAE,MAAM;IAIf,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;IAI1B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAIlC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;CAG1B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createCookieSessionStorage, } from '@shopify/remix-oxygen';
|
|
2
|
+
export class PreviewSession {
|
|
3
|
+
#sessionStorage;
|
|
4
|
+
#session;
|
|
5
|
+
constructor(sessionStorage, session) {
|
|
6
|
+
this.#sessionStorage = sessionStorage;
|
|
7
|
+
this.#session = session;
|
|
8
|
+
}
|
|
9
|
+
static async init(request, secrets) {
|
|
10
|
+
const storage = createCookieSessionStorage({
|
|
11
|
+
cookie: {
|
|
12
|
+
name: '__preview',
|
|
13
|
+
sameSite: 'none',
|
|
14
|
+
secure: true,
|
|
15
|
+
secrets,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const session = await storage.getSession(request.headers.get('Cookie'));
|
|
19
|
+
return new this(storage, session);
|
|
20
|
+
}
|
|
21
|
+
has(key) {
|
|
22
|
+
return this.#session.has(key);
|
|
23
|
+
}
|
|
24
|
+
get(key) {
|
|
25
|
+
return this.#session.get(key);
|
|
26
|
+
}
|
|
27
|
+
destroy() {
|
|
28
|
+
return this.#sessionStorage.destroySession(this.#session);
|
|
29
|
+
}
|
|
30
|
+
set(key, value) {
|
|
31
|
+
this.#session.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
commit() {
|
|
34
|
+
return this.#sessionStorage.commitSession(this.#session);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pack/hydrogen",
|
|
3
|
+
"description": "Pack Hydrogen",
|
|
4
|
+
"version": "0.0.2",
|
|
5
|
+
"exports": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=16"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"clean": "npx rimraf dist",
|
|
15
|
+
"build": "npx tsc",
|
|
16
|
+
"test": "",
|
|
17
|
+
"test:watch": "npx vitest"
|
|
18
|
+
},
|
|
19
|
+
"author": "Pack",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@pack/client": "^0.0.5",
|
|
26
|
+
"@shopify/hydrogen": "^2023.10.2",
|
|
27
|
+
"@shopify/remix-oxygen": "^2.0.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@shopify/oxygen-workers-types": "^4.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|