@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 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
+ }
@@ -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
+ }