diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c7b5e0de2..21140c392 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -21,6 +21,11 @@ jobs:
node-version: 18
check-latest: true
- run: corepack enable
+ - name: Install Deno
+ uses: denoland/setup-deno@v1
+ with:
+ # Should satisfy the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/main/node/bridge.ts#L17
+ deno-version: v1
- name: Install
run: pnpm install
- name: Build
diff --git a/.gitignore b/.gitignore
index f8b68daa0..529f2ec7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -139,3 +139,6 @@ build/
/playwright-report/
/blob-report/
/playwright/.cache/
+
+# Generated by `deno types`
+/packages/remix-edge-adapter/deno.d.ts
diff --git a/README.md b/README.md
index 1499f57b2..4f171aaef 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,11 @@ are three packages:
- `@netlify/remix-edge-adapter` - The Remix adapter for Netlify Edge Functions
- `@netlify/remix-runtime` - The Remix runtime for Netlify Edge Functions
+## Hydrogen
+
+Shopify Hydrogen sites are supported and automatically detected. However, only
+[the edge adapter](./packages/remix-edge-adapter/README.md) is supported, and only when using Remix Vite.
+
## Development
### Installation
diff --git a/packages/remix-adapter/README.md b/packages/remix-adapter/README.md
index d2075a543..c0c37b09a 100644
--- a/packages/remix-adapter/README.md
+++ b/packages/remix-adapter/README.md
@@ -6,3 +6,6 @@ The Remix Adapter for Netlify allows you to deploy your [Remix](https://remix.ru
It is strongly advised to use [the Netlify Remix template](https://github.com/netlify/remix-template) to create a Remix
site for deployment to Netlify. See [Remix on Netlify](https://docs.netlify.com/frameworks/remix/) for more details and
other options.
+
+Please note that this adapter **does not support Hydrogen**. Hydrogen is only supported via Edge Functions. See
+.
diff --git a/packages/remix-adapter/src/vite/plugin.ts b/packages/remix-adapter/src/vite/plugin.ts
index 325503176..7c6d066ad 100644
--- a/packages/remix-adapter/src/vite/plugin.ts
+++ b/packages/remix-adapter/src/vite/plugin.ts
@@ -4,23 +4,35 @@ import { join, relative, sep } from 'node:path'
import { sep as posixSep } from 'node:path/posix'
import { version, name } from '../../package.json'
-const SERVER_ID = 'virtual:netlify-server'
-const RESOLVED_SERVER_ID = `\0${SERVER_ID}`
+const NETLIFY_FUNCTIONS_DIR = '.netlify/functions-internal'
+
+const FUNCTION_FILENAME = 'remix-server.mjs'
+/**
+ * The chunk filename without an extension, i.e. in the Rollup config `input` format
+ */
+const FUNCTION_HANDLER_CHUNK = 'server'
+
+const FUNCTION_HANDLER_MODULE_ID = 'virtual:netlify-server'
+const RESOLVED_FUNCTION_HANDLER_MODULE_ID = `\0${FUNCTION_HANDLER_MODULE_ID}`
const toPosixPath = (path: string) => path.split(sep).join(posixSep)
-// The virtual module that is the compiled server entrypoint.
-const serverCode = /* js */ `
+// The virtual module that is the compiled Vite SSR entrypoint (a Netlify Function handler)
+const FUNCTION_HANDLER = /* js */ `
import { createRequestHandler } from "@netlify/remix-adapter";
import * as build from "virtual:remix/server-build";
-export default createRequestHandler({ build });
+export default createRequestHandler({
+ build,
+ getLoadContext: async (_req, ctx) => ctx,
+});
`
// This is written to the functions directory. It just re-exports
// the compiled entrypoint, along with Netlify function config.
-function generateNetlifyFunction(server: string) {
+function generateNetlifyFunction(handlerPath: string) {
return /* js */ `
- export { default } from "${server}";
+ export { default } from "${handlerPath}";
+
export const config = {
name: "Remix server handler",
generator: "${name}@${version}",
@@ -41,12 +53,18 @@ export function netlifyPlugin(): Plugin {
isSsr = isSsrBuild
if (command === 'build') {
if (isSsrBuild) {
- // We need to add an extra entrypoint, as we need to compile
+ // We need to add an extra SSR entrypoint, as we need to compile
// the server entrypoint too. This is because it uses virtual
// modules.
+ // NOTE: the below is making various assumptions about the Remix Vite plugin's
+ // implementation details:
+ // https://github.com/remix-run/remix/blob/cc65962b1a96d1e134336aa9620ef1dad7c5efb1/packages/remix-dev/vite/plugin.ts#L1149-L1168
+ // TODO(serhalp) Stop making these assumptions or assert them explictly.
+ // TODO(serhalp) Unless I'm misunderstanding something, we should only need to *replace*
+ // the default Remix Vite SSR entrypoint, not add an additional one.
if (typeof config.build?.rollupOptions?.input === 'string') {
config.build.rollupOptions.input = {
- server: SERVER_ID,
+ [FUNCTION_HANDLER_CHUNK]: FUNCTION_HANDLER_MODULE_ID,
index: config.build.rollupOptions.input,
}
if (config.build.rollupOptions.output && !Array.isArray(config.build.rollupOptions.output)) {
@@ -57,14 +75,14 @@ export function netlifyPlugin(): Plugin {
}
},
async resolveId(source) {
- if (source === SERVER_ID) {
- return RESOLVED_SERVER_ID
+ if (source === FUNCTION_HANDLER_MODULE_ID) {
+ return RESOLVED_FUNCTION_HANDLER_MODULE_ID
}
},
// See https://vitejs.dev/guide/api-plugin#virtual-modules-convention.
load(id) {
- if (id === RESOLVED_SERVER_ID) {
- return serverCode
+ if (id === RESOLVED_FUNCTION_HANDLER_MODULE_ID) {
+ return FUNCTION_HANDLER
}
},
async configResolved(config) {
@@ -74,14 +92,14 @@ export function netlifyPlugin(): Plugin {
async writeBundle() {
// Write the server entrypoint to the Netlify functions directory
if (currentCommand === 'build' && isSsr) {
- const functionsDirectory = join(resolvedConfig.root, '.netlify/functions-internal')
+ const functionsDirectory = join(resolvedConfig.root, NETLIFY_FUNCTIONS_DIR)
await mkdir(functionsDirectory, { recursive: true })
- const serverPath = join(resolvedConfig.build.outDir, 'server.js')
- const relativeServerPath = toPosixPath(relative(functionsDirectory, serverPath))
+ const handlerPath = join(resolvedConfig.build.outDir, `${FUNCTION_HANDLER_CHUNK}.js`)
+ const relativeHandlerPath = toPosixPath(relative(functionsDirectory, handlerPath))
- await writeFile(join(functionsDirectory, 'remix-server.mjs'), generateNetlifyFunction(relativeServerPath))
+ await writeFile(join(functionsDirectory, FUNCTION_FILENAME), generateNetlifyFunction(relativeHandlerPath))
}
},
}
diff --git a/packages/remix-edge-adapter/README.md b/packages/remix-edge-adapter/README.md
index 5267162a0..f923e4d41 100644
--- a/packages/remix-edge-adapter/README.md
+++ b/packages/remix-edge-adapter/README.md
@@ -3,6 +3,35 @@
The Remix Edge Adapter for Netlify allows you to deploy your [Remix](https://remix.run) app to
[Netlify Edge Functions](https://docs.netlify.com/edge-functions/overview/).
+## Usage
+
It is strongly advised to use [the Netlify Remix template](https://github.com/netlify/remix-template) to create a Remix
site for deployment to Netlify. See [Remix on Netlify](https://docs.netlify.com/frameworks/remix/) for more details and
other options.
+
+However, if you are using **Remix Vite**, you can instead deploy your existing site to Netlify by following these steps:
+
+1. Add dependencies on `@netlify/remix-edge-adapter` and `@netlify/remix-runtime`
+2. Use the Netlify Remix edge Vite plugin in your Vite config:
+
+```js
+// vite.config.js
+import { vitePlugin as remix } from "@remix-run/dev";
+import { netlifyPlugin } from "@netlify/remix-edge-adapter/plugin";
+
+export default defineConfig({
+ plugins: [remix(), netlifyPlugin(),
+});
+```
+
+3. Add an `app/entry.jsx` (.tsx if using TypeScript) with these contents:
+
+```js
+// app.entry.jsx or .tsx
+export { default } from 'virtual:netlify-server-entry'
+```
+
+### Hydrogen
+
+Hydrogen Vite sites are supported and automatically detected. However, additional setup is required. See
+ for details.
diff --git a/packages/remix-edge-adapter/package.json b/packages/remix-edge-adapter/package.json
index 1621779ae..29c5c6f11 100644
--- a/packages/remix-edge-adapter/package.json
+++ b/packages/remix-edge-adapter/package.json
@@ -40,6 +40,7 @@
],
"scripts": {
"prepack": "pnpm run build",
+ "postinstall": "deno types > deno.d.ts",
"build": "tsup-node src/index.ts src/vite/plugin.ts --format esm,cjs --dts --target node16 --clean",
"build:watch": "pnpm run build --watch"
},
@@ -59,10 +60,10 @@
"homepage": "https://github.com/netlify/remix-compute#readme",
"dependencies": {
"@netlify/remix-runtime": "2.3.0",
+ "@remix-run/dev": "^2.9.2",
"isbot": "^5.0.0"
},
"devDependencies": {
- "@remix-run/dev": "^2.9.2",
"@remix-run/react": "^2.9.2",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
diff --git a/packages/remix-edge-adapter/src/common/server.ts b/packages/remix-edge-adapter/src/common/server.ts
index 332705d11..a26946c46 100644
--- a/packages/remix-edge-adapter/src/common/server.ts
+++ b/packages/remix-edge-adapter/src/common/server.ts
@@ -2,7 +2,7 @@ import type { AppLoadContext, ServerBuild } from '@netlify/remix-runtime'
import { createRequestHandler as createRemixRequestHandler } from '@netlify/remix-runtime'
import type { Context } from '@netlify/edge-functions'
-type LoadContext = AppLoadContext & Context
+export type LoadContext = AppLoadContext & Context
/**
* A function that returns the value to use as `context` in route `loader` and
@@ -49,10 +49,10 @@ export function createRequestHandler({
if (response.status === 404) {
// Check if there is a matching static file
- const originResponse = await context.next({
+ const originResponse = await loadContext.next({
sendConditionalRequest: true,
})
- if (originResponse.status !== 404) {
+ if (originResponse && originResponse?.status !== 404) {
return originResponse
}
}
diff --git a/packages/remix-edge-adapter/src/index.ts b/packages/remix-edge-adapter/src/index.ts
index 4fca1f417..9f79b8451 100644
--- a/packages/remix-edge-adapter/src/index.ts
+++ b/packages/remix-edge-adapter/src/index.ts
@@ -4,3 +4,4 @@ export type { GetLoadContextFunction, RequestHandler } from './common/server'
export { createRequestHandler } from './common/server'
export { config } from './classic-compiler/defaultRemixConfig'
export { default as handleRequest } from './common/entry.server'
+export { createHydrogenAppLoadContext } from './vite/hydrogen'
diff --git a/packages/remix-edge-adapter/src/vite/hydrogen.ts b/packages/remix-edge-adapter/src/vite/hydrogen.ts
new file mode 100644
index 000000000..472738e66
--- /dev/null
+++ b/packages/remix-edge-adapter/src/vite/hydrogen.ts
@@ -0,0 +1,64 @@
+import type { Context } from '@netlify/edge-functions'
+
+/**
+ * The base Hydrogen templates expect a globally defined `ExecutionContext` type, which by default
+ * comes from Oxygen:
+ * https://github.com/Shopify/hydrogen/blob/92a53c477540ee22cc273e7f3cbd2fd0582c815f/templates/skeleton/env.d.ts#L3.
+ * We do the same thing to minimize differences.
+ */
+declare global {
+ interface ExecutionContext {
+ waitUntil(promise: Promise): void
+ }
+}
+
+/**
+ * For convenience, this matches the function signature that Hydrogen includes by default in its templates:
+ * https://github.com/Shopify/hydrogen/blob/92a53c477540ee22cc273e7f3cbd2fd0582c815f/templates/skeleton/app/lib/context.ts.
+ *
+ * Remix expects the user to use module augmentation to modify their exported `AppLoadContext` type. See
+ * https://github.com/remix-run/remix/blob/5dc3b67dc31f3df7b1b0298ae4e9cac9c5ae1c06/packages/remix-server-runtime/data.ts#L15-L23
+ * Hydrogen follows this pattern. However, because of the way TypeScript module augmentation works,
+ * we can't access the final user-augmented type here, so we have to do this dance with generic types.
+ */
+type CreateAppLoadContext = (
+ request: Request,
+ env: E,
+ executionContext: ExecutionContext,
+) => Promise
+
+const executionContext: ExecutionContext = {
+ /**
+ * Hydrogen expects a `waitUntil` function like the one in the workerd runtime:
+ * https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil.
+ * Netlify Edge Functions don't have such a function, but Deno Deploy isolates make a best-effort
+ * attempt to wait for the event loop to drain, so just awaiting the promise here is equivalent.
+ */
+ async waitUntil(p: Promise): Promise {
+ await p
+ },
+}
+
+/**
+ * In dev we run in a Node.js environment (via Remix Vite) but otherwise we run in a Deno (Netlify
+ * Edge Functions) environment.
+ */
+const getEnv = () => {
+ if (globalThis.Netlify) {
+ return globalThis.Netlify.env.toObject()
+ }
+ return process.env
+}
+
+export const createHydrogenAppLoadContext = async (
+ request: Request,
+ netlifyContext: Context,
+ createAppLoadContext: CreateAppLoadContext,
+): Promise> => {
+ const env = getEnv() as E
+ const userHydrogenContext = await createAppLoadContext(request, env, executionContext)
+
+ // NOTE: We use `Object.assign` here because a spread would access the getters on the
+ // `netlifyContext` fields, some of which throw a "not implemented" error in local dev.
+ return Object.assign(netlifyContext, userHydrogenContext)
+}
diff --git a/packages/remix-edge-adapter/src/vite/plugin.ts b/packages/remix-edge-adapter/src/vite/plugin.ts
index 4f4b7bfee..7e0aff9bf 100644
--- a/packages/remix-edge-adapter/src/vite/plugin.ts
+++ b/packages/remix-edge-adapter/src/vite/plugin.ts
@@ -1,27 +1,93 @@
import type { Plugin, ResolvedConfig } from 'vite'
-import { writeFile, mkdir, readdir } from 'node:fs/promises'
+import { fromNodeRequest, toNodeRequest } from '@remix-run/dev/dist/vite/node-adapter.js'
+import type { EdgeFunction, Context as NetlifyContext } from '@netlify/edge-functions'
+import { writeFile, mkdir, readdir, access } from 'node:fs/promises'
import { join, relative, sep } from 'node:path'
import { sep as posixSep } from 'node:path/posix'
-import { version, name } from '../../package.json'
import { isBuiltin } from 'node:module'
+import { version, name } from '../../package.json'
+
+const NETLIFY_EDGE_FUNCTIONS_DIR = '.netlify/edge-functions'
-const SERVER_ID = 'virtual:netlify-server'
-const RESOLVED_SERVER_ID = `\0${SERVER_ID}`
+const EDGE_FUNCTION_FILENAME = 'remix-server.mjs'
+/**
+ * The chunk filename without an extension, i.e. in the Rollup config `input` format
+ */
+const EDGE_FUNCTION_HANDLER_CHUNK = 'server'
+
+const EDGE_FUNCTION_HANDLER_MODULE_ID = 'virtual:netlify-server'
+const RESOLVED_EDGE_FUNCTION_HANDLER_MODULE_ID = `\0${EDGE_FUNCTION_HANDLER_MODULE_ID}`
const toPosixPath = (path: string) => path.split(sep).join(posixSep)
-// The virtual module that is the compiled server entrypoint.
-const serverCode = /* js */ `
+const notImplemented = () => {
+ throw new Error(`
+This is a fake Netlify context object for local dev. It is not supported here, but it will work with
+\`netlify serve\` and in a production build. To fix this, add it as custom context in your
+\`createAppLoadContext\` conditionally in dev.
+`)
+}
+const getFakeNetlifyContext = () =>
+ ({
+ requestId: 'fake-netlify-request-id-for-dev',
+ next: async () => new Response('', { status: 404 }),
+ geo: {
+ city: 'Mock City',
+ country: { code: 'MC', name: 'Mock Country' },
+ subdivision: { code: 'MS', name: 'Mock Subdivision' },
+ longitude: 0,
+ latitude: 0,
+ timezone: 'UTC',
+ },
+ get cookies() {
+ return notImplemented()
+ },
+ get deploy() {
+ return notImplemented()
+ },
+ get ip() {
+ return notImplemented()
+ },
+ get json() {
+ return notImplemented()
+ },
+ get log() {
+ return notImplemented()
+ },
+ get params() {
+ return notImplemented()
+ },
+ get rewrite() {
+ return notImplemented()
+ },
+ get site() {
+ return notImplemented()
+ },
+ get account() {
+ return notImplemented()
+ },
+ get server() {
+ return notImplemented()
+ },
+ }) as NetlifyContext
+
+// The virtual module that is the compiled Vite SSR entrypoint (a Netlify Edge Function handler)
+const EDGE_FUNCTION_HANDLER = /* js */ `
import { createRequestHandler } from "@netlify/remix-edge-adapter";
import * as build from "virtual:remix/server-build";
-export default createRequestHandler({ build });
+
+export default createRequestHandler({
+ build,
+ getLoadContext: async (_req, ctx) => ctx,
+});
`
// This is written to the edge functions directory. It just re-exports
// the compiled entrypoint, along with the Netlify function config.
-function generateEntrypoint(server: string, exclude: Array = []) {
+function generateEdgeFunction(handlerPath: string, exclude: Array = []) {
return /* js */ `
- export { default } from "${server}";
+ export { default } from "${handlerPath}";
+
export const config = {
name: "Remix server handler",
generator: "${name}@${version}",
@@ -31,10 +97,38 @@ function generateEntrypoint(server: string, exclude: Array = []) {
};`
}
+// Note: these are checked in order. The first match is used.
+const ALLOWED_USER_EDGE_FUNCTION_HANDLER_FILENAMES = [
+ 'server.ts',
+ 'server.mts',
+ 'server.cts',
+ 'server.mjs',
+ 'server.cjs',
+ 'server.js',
+]
+const findUserEdgeFunctionHandlerFile = async (root: string) => {
+ for (const filename of ALLOWED_USER_EDGE_FUNCTION_HANDLER_FILENAMES) {
+ try {
+ await access(join(root, filename))
+ return filename
+ } catch {}
+ }
+
+ throw new Error(
+ 'Your Hydrogen site must include a `server.ts` (or js/mjs/cjs/mts/cts) file at the root to deploy to Netlify. See https://github.com/netlify/hydrogen-template.',
+ )
+}
+
+const getEdgeFunctionHandlerModuleId = async (root: string, isHydrogenSite: boolean) => {
+ if (!isHydrogenSite) return EDGE_FUNCTION_HANDLER_MODULE_ID
+ return findUserEdgeFunctionHandlerFile(root)
+}
+
export function netlifyPlugin(): Plugin {
let resolvedConfig: ResolvedConfig
let currentCommand: string
let isSsr: boolean | undefined
+ let isHydrogenSite: boolean
return {
name: 'vite-plugin-remix-netlify-edge',
@@ -43,31 +137,53 @@ export function netlifyPlugin(): Plugin {
isSsr = isSsrBuild
if (command === 'build') {
if (isSsrBuild) {
- // Configure for edge functions
+ // Configure for Netlify Edge Functions
config.ssr = {
...config.ssr,
target: 'webworker',
// Only externalize Node builtins
noExternal: /^(?!node:).*$/,
}
+ }
+ }
+ },
+ configResolved: {
+ order: 'pre',
+ async handler(config) {
+ resolvedConfig = config
+ isHydrogenSite = resolvedConfig.plugins.find((plugin) => plugin.name === 'hydrogen:main') != null
+
+ if (currentCommand === 'build' && isSsr) {
// We need to add an extra entrypoint, as we need to compile
// the server entrypoint too. This is because it uses virtual
- // modules. It also avoids the faff of dealing with npm modules
- // in Deno.
+ // modules. It also avoids the faff of dealing with npm modules in Deno.
+ // NOTE: the below is making various assumptions about the Remix Vite plugin's
+ // implementation details:
+ // https://github.com/remix-run/remix/blob/cc65962b1a96d1e134336aa9620ef1dad7c5efb1/packages/remix-dev/vite/plugin.ts#L1149-L1168
+ // TODO(serhalp) Stop making these assumptions or assert them explictly.
+ // TODO(serhalp) Unless I'm misunderstanding something, we should only need to *replace*
+ // the default Remix Vite SSR entrypoint, not add an additional one.
if (typeof config.build?.rollupOptions?.input === 'string') {
+ const edgeFunctionHandlerModuleId = await getEdgeFunctionHandlerModuleId(
+ resolvedConfig.root,
+ isHydrogenSite,
+ )
+
config.build.rollupOptions.input = {
- server: SERVER_ID,
+ [EDGE_FUNCTION_HANDLER_CHUNK]: edgeFunctionHandlerModuleId,
index: config.build.rollupOptions.input,
}
if (config.build.rollupOptions.output && !Array.isArray(config.build.rollupOptions.output)) {
- config.build.rollupOptions.output.entryFileNames = '[name].js'
+ // NOTE: must use function syntax here to work around https://github.com/Shopify/hydrogen/issues/2496
+ config.build.rollupOptions.output.entryFileNames = () => '[name].js'
}
}
+ } else if (isHydrogenSite && currentCommand === 'serve') {
+ // handle dev server and use user's server.ts as entrypoint
+ // to later use it for handling requests within vite's dev server middleware
+ config.build.ssr = await findUserEdgeFunctionHandlerFile(resolvedConfig.root)
}
- }
- },
- async configResolved(config) {
- resolvedConfig = config
+ },
},
resolveId: {
@@ -92,8 +208,8 @@ export function netlifyPlugin(): Plugin {
}
// Our virtual entrypoint module. See
// https://vitejs.dev/guide/api-plugin#virtual-modules-convention.
- if (source === SERVER_ID) {
- return RESOLVED_SERVER_ID
+ if (source === EDGE_FUNCTION_HANDLER_MODULE_ID) {
+ return RESOLVED_EDGE_FUNCTION_HANDLER_MODULE_ID
}
if (isSsr && isBuiltin(source)) {
@@ -109,10 +225,40 @@ export function netlifyPlugin(): Plugin {
},
// See https://vitejs.dev/guide/api-plugin#virtual-modules-convention.
load(id) {
- if (id === RESOLVED_SERVER_ID) {
- return serverCode
+ if (id === RESOLVED_EDGE_FUNCTION_HANDLER_MODULE_ID) {
+ return EDGE_FUNCTION_HANDLER
}
},
+
+ configureServer: {
+ order: 'pre',
+ handler(viteDevServer) {
+ return () => {
+ if (isHydrogenSite && !viteDevServer.config.server.middlewareMode) {
+ viteDevServer.middlewares.use(async (nodeReq, nodeRes, next) => {
+ try {
+ const edgeFunctionHandlerModuleId = await findUserEdgeFunctionHandlerFile(resolvedConfig.root)
+ let build = (await viteDevServer.ssrLoadModule(edgeFunctionHandlerModuleId)) as {
+ default: EdgeFunction
+ }
+ const handleRequest = build.default
+ let req = fromNodeRequest(nodeReq)
+ const res = await handleRequest(req, getFakeNetlifyContext())
+ if (res instanceof Response) return await toNodeRequest(res, nodeRes)
+ if (res instanceof URL) {
+ next(new Error('URLs are not supported in dev server middleware'))
+ return
+ }
+ next()
+ } catch (error) {
+ next(error)
+ }
+ })
+ }
+ }
+ },
+ },
+
// See https://rollupjs.org/plugin-development/#writebundle.
async writeBundle() {
// Write the server entrypoint to the Netlify functions directory
@@ -134,16 +280,16 @@ export function netlifyPlugin(): Plugin {
// Ignore if it doesn't exist
}
- const edgeFunctionsDirectory = join(resolvedConfig.root, '.netlify/edge-functions')
+ const edgeFunctionsDirectory = join(resolvedConfig.root, NETLIFY_EDGE_FUNCTIONS_DIR)
await mkdir(edgeFunctionsDirectory, { recursive: true })
- const serverPath = join(resolvedConfig.build.outDir, 'server.js')
- const relativeServerPath = toPosixPath(relative(edgeFunctionsDirectory, serverPath))
+ const handlerPath = join(resolvedConfig.build.outDir, `${EDGE_FUNCTION_HANDLER_CHUNK}.js`)
+ const relativeHandlerPath = toPosixPath(relative(edgeFunctionsDirectory, handlerPath))
await writeFile(
- join(edgeFunctionsDirectory, 'remix-server.mjs'),
- generateEntrypoint(relativeServerPath, exclude),
+ join(edgeFunctionsDirectory, EDGE_FUNCTION_FILENAME),
+ generateEdgeFunction(relativeHandlerPath, exclude),
)
}
},
diff --git a/packages/remix-edge-adapter/tsconfig.json b/packages/remix-edge-adapter/tsconfig.json
index 2325b1b6e..aee7b481c 100644
--- a/packages/remix-edge-adapter/tsconfig.json
+++ b/packages/remix-edge-adapter/tsconfig.json
@@ -5,5 +5,5 @@
"jsx": "react-jsx",
"outDir": "./build"
},
- "include": ["./src"]
+ "include": ["deno.d.ts", "./src"]
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6f099dca1..7d0d6ab10 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -353,13 +353,13 @@ importers:
'@netlify/remix-runtime':
specifier: 2.3.0
version: link:../remix-runtime
+ '@remix-run/dev':
+ specifier: ^2.9.2
+ version: 2.11.1(@remix-run/react@2.11.1)(@remix-run/serve@2.11.1)(@types/node@20.16.3)(ts-node@10.9.2)(typescript@5.4.5)(vite@5.3.5)
isbot:
specifier: ^5.0.0
version: 5.1.12
devDependencies:
- '@remix-run/dev':
- specifier: ^2.9.2
- version: 2.11.1(@remix-run/react@2.11.1)(@remix-run/serve@2.11.1)(@types/node@20.16.3)(ts-node@10.9.2)(typescript@5.4.5)(vite@5.3.5)
'@remix-run/react':
specifier: ^2.9.2
version: 2.11.1(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)
@@ -399,7 +399,6 @@ packages:
dependencies:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
- dev: true
/@babel/code-frame@7.12.11:
resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==}
@@ -413,12 +412,10 @@ packages:
dependencies:
'@babel/highlight': 7.24.7
picocolors: 1.0.1
- dev: true
/@babel/compat-data@7.25.2:
resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==}
engines: {node: '>=6.9.0'}
- dev: true
/@babel/core@7.24.5:
resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==}
@@ -464,7 +461,6 @@ packages:
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/eslint-parser@7.24.5(@babel/core@7.24.5)(eslint@8.57.0):
resolution: {integrity: sha512-gsUcqS/fPlgAw1kOtpss7uhY6E9SFFANQ6EFX5GTvzUwaV0+sGaZWk6xq22MOdeT9wfxyokW3ceCUvOiRtZciQ==}
@@ -502,14 +498,12 @@ packages:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
jsesc: 2.5.2
- dev: true
/@babel/helper-annotate-as-pure@7.24.7:
resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.25.2
- dev: true
/@babel/helper-compilation-targets@7.25.2:
resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==}
@@ -520,7 +514,6 @@ packages:
browserslist: 4.23.3
lru-cache: 5.1.1
semver: 6.3.1
- dev: true
/@babel/helper-create-class-features-plugin@7.25.0(@babel/core@7.25.2):
resolution: {integrity: sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==}
@@ -538,7 +531,6 @@ packages:
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/helper-member-expression-to-functions@7.24.8:
resolution: {integrity: sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==}
@@ -548,7 +540,6 @@ packages:
'@babel/types': 7.25.2
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/helper-module-imports@7.24.7:
resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==}
@@ -558,7 +549,6 @@ packages:
'@babel/types': 7.25.2
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/helper-module-transforms@7.25.2(@babel/core@7.24.5):
resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==}
@@ -588,19 +578,16 @@ packages:
'@babel/traverse': 7.25.3
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/helper-optimise-call-expression@7.24.7:
resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.25.2
- dev: true
/@babel/helper-plugin-utils@7.24.8:
resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==}
engines: {node: '>=6.9.0'}
- dev: true
/@babel/helper-replace-supers@7.25.0(@babel/core@7.25.2):
resolution: {integrity: sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==}
@@ -614,7 +601,6 @@ packages:
'@babel/traverse': 7.25.3
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/helper-simple-access@7.24.7:
resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==}
@@ -624,7 +610,6 @@ packages:
'@babel/types': 7.25.2
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/helper-skip-transparent-expression-wrappers@7.24.7:
resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==}
@@ -634,22 +619,18 @@ packages:
'@babel/types': 7.25.2
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/helper-string-parser@7.24.8:
resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==}
engines: {node: '>=6.9.0'}
- dev: true
/@babel/helper-validator-identifier@7.24.7:
resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==}
engines: {node: '>=6.9.0'}
- dev: true
/@babel/helper-validator-option@7.24.8:
resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==}
engines: {node: '>=6.9.0'}
- dev: true
/@babel/helpers@7.25.0:
resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==}
@@ -657,7 +638,6 @@ packages:
dependencies:
'@babel/template': 7.25.0
'@babel/types': 7.25.2
- dev: true
/@babel/highlight@7.24.7:
resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
@@ -667,7 +647,6 @@ packages:
chalk: 2.4.2
js-tokens: 4.0.0
picocolors: 1.0.1
- dev: true
/@babel/parser@7.25.3:
resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==}
@@ -675,7 +654,6 @@ packages:
hasBin: true
dependencies:
'@babel/types': 7.25.2
- dev: true
/@babel/plugin-syntax-decorators@7.24.7(@babel/core@7.25.2):
resolution: {integrity: sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==}
@@ -685,7 +663,6 @@ packages:
dependencies:
'@babel/core': 7.25.2
'@babel/helper-plugin-utils': 7.24.8
- dev: true
/@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.25.2):
resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==}
@@ -695,7 +672,6 @@ packages:
dependencies:
'@babel/core': 7.25.2
'@babel/helper-plugin-utils': 7.24.8
- dev: true
/@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.25.2):
resolution: {integrity: sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==}
@@ -705,7 +681,6 @@ packages:
dependencies:
'@babel/core': 7.25.2
'@babel/helper-plugin-utils': 7.24.8
- dev: true
/@babel/plugin-transform-modules-commonjs@7.24.8(@babel/core@7.25.2):
resolution: {integrity: sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==}
@@ -719,7 +694,6 @@ packages:
'@babel/helper-simple-access': 7.24.7
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.25.2):
resolution: {integrity: sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==}
@@ -784,7 +758,6 @@ packages:
'@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.25.2)
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/preset-react@7.24.7(@babel/core@7.25.2):
resolution: {integrity: sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==}
@@ -817,7 +790,6 @@ packages:
'@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.25.2)
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/runtime@7.24.5:
resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==}
@@ -831,7 +803,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
- dev: true
/@babel/template@7.25.0:
resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==}
@@ -840,7 +811,6 @@ packages:
'@babel/code-frame': 7.24.7
'@babel/parser': 7.25.3
'@babel/types': 7.25.2
- dev: true
/@babel/traverse@7.25.3:
resolution: {integrity: sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==}
@@ -855,7 +825,6 @@ packages:
globals: 11.12.0
transitivePeerDependencies:
- supports-color
- dev: true
/@babel/types@7.25.2:
resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==}
@@ -864,7 +833,6 @@ packages:
'@babel/helper-string-parser': 7.24.8
'@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0
- dev: true
/@bugsnag/browser@7.25.0:
resolution: {integrity: sha512-PzzWy5d9Ly1CU1KkxTB6ZaOw/dO+CYSfVtqxVJccy832e6+7rW/dvSw5Jy7rsNhgcKSKjZq86LtNkPSvritOLA==}
@@ -1241,7 +1209,6 @@ packages:
engines: {node: '>=12'}
dependencies:
'@jridgewell/trace-mapping': 0.3.9
- dev: true
/@dabh/diagnostics@2.0.3:
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
@@ -1261,7 +1228,6 @@ packages:
/@emotion/hash@0.9.2:
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
- dev: true
/@esbuild/aix-ppc64@0.19.11:
resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==}
@@ -1269,7 +1235,6 @@ packages:
cpu: [ppc64]
os: [aix]
requiresBuild: true
- dev: true
optional: true
/@esbuild/aix-ppc64@0.21.2:
@@ -1287,7 +1252,6 @@ packages:
cpu: [ppc64]
os: [aix]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-arm64@0.17.6:
@@ -1296,7 +1260,6 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-arm64@0.19.11:
@@ -1305,7 +1268,6 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-arm64@0.21.2:
@@ -1323,7 +1285,6 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-arm@0.17.6:
@@ -1332,7 +1293,6 @@ packages:
cpu: [arm]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-arm@0.19.11:
@@ -1341,7 +1301,6 @@ packages:
cpu: [arm]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-arm@0.21.2:
@@ -1359,7 +1318,6 @@ packages:
cpu: [arm]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-x64@0.17.6:
@@ -1368,7 +1326,6 @@ packages:
cpu: [x64]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-x64@0.19.11:
@@ -1377,7 +1334,6 @@ packages:
cpu: [x64]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/android-x64@0.21.2:
@@ -1395,7 +1351,6 @@ packages:
cpu: [x64]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@esbuild/darwin-arm64@0.17.6:
@@ -1404,7 +1359,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
- dev: true
optional: true
/@esbuild/darwin-arm64@0.19.11:
@@ -1413,7 +1367,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
- dev: true
optional: true
/@esbuild/darwin-arm64@0.21.2:
@@ -1431,7 +1384,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
- dev: true
optional: true
/@esbuild/darwin-x64@0.17.6:
@@ -1440,7 +1392,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
- dev: true
optional: true
/@esbuild/darwin-x64@0.19.11:
@@ -1449,7 +1400,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
- dev: true
optional: true
/@esbuild/darwin-x64@0.21.2:
@@ -1467,7 +1417,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
- dev: true
optional: true
/@esbuild/freebsd-arm64@0.17.6:
@@ -1476,7 +1425,6 @@ packages:
cpu: [arm64]
os: [freebsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/freebsd-arm64@0.19.11:
@@ -1485,7 +1433,6 @@ packages:
cpu: [arm64]
os: [freebsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/freebsd-arm64@0.21.2:
@@ -1503,7 +1450,6 @@ packages:
cpu: [arm64]
os: [freebsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/freebsd-x64@0.17.6:
@@ -1512,7 +1458,6 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/freebsd-x64@0.19.11:
@@ -1521,7 +1466,6 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/freebsd-x64@0.21.2:
@@ -1539,7 +1483,6 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-arm64@0.17.6:
@@ -1548,7 +1491,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-arm64@0.19.11:
@@ -1557,7 +1499,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-arm64@0.21.2:
@@ -1575,7 +1516,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-arm@0.17.6:
@@ -1584,7 +1524,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-arm@0.19.11:
@@ -1593,7 +1532,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-arm@0.21.2:
@@ -1611,7 +1549,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-ia32@0.17.6:
@@ -1620,7 +1557,6 @@ packages:
cpu: [ia32]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-ia32@0.19.11:
@@ -1629,7 +1565,6 @@ packages:
cpu: [ia32]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-ia32@0.21.2:
@@ -1647,7 +1582,6 @@ packages:
cpu: [ia32]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-loong64@0.17.6:
@@ -1656,7 +1590,6 @@ packages:
cpu: [loong64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-loong64@0.19.11:
@@ -1665,7 +1598,6 @@ packages:
cpu: [loong64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-loong64@0.21.2:
@@ -1683,7 +1615,6 @@ packages:
cpu: [loong64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-mips64el@0.17.6:
@@ -1692,7 +1623,6 @@ packages:
cpu: [mips64el]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-mips64el@0.19.11:
@@ -1701,7 +1631,6 @@ packages:
cpu: [mips64el]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-mips64el@0.21.2:
@@ -1719,7 +1648,6 @@ packages:
cpu: [mips64el]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-ppc64@0.17.6:
@@ -1728,7 +1656,6 @@ packages:
cpu: [ppc64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-ppc64@0.19.11:
@@ -1737,7 +1664,6 @@ packages:
cpu: [ppc64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-ppc64@0.21.2:
@@ -1755,7 +1681,6 @@ packages:
cpu: [ppc64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-riscv64@0.17.6:
@@ -1764,7 +1689,6 @@ packages:
cpu: [riscv64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-riscv64@0.19.11:
@@ -1773,7 +1697,6 @@ packages:
cpu: [riscv64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-riscv64@0.21.2:
@@ -1791,7 +1714,6 @@ packages:
cpu: [riscv64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-s390x@0.17.6:
@@ -1800,7 +1722,6 @@ packages:
cpu: [s390x]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-s390x@0.19.11:
@@ -1809,7 +1730,6 @@ packages:
cpu: [s390x]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-s390x@0.21.2:
@@ -1827,7 +1747,6 @@ packages:
cpu: [s390x]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-x64@0.17.6:
@@ -1836,7 +1755,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-x64@0.19.11:
@@ -1845,7 +1763,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/linux-x64@0.21.2:
@@ -1863,7 +1780,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@esbuild/netbsd-x64@0.17.6:
@@ -1872,7 +1788,6 @@ packages:
cpu: [x64]
os: [netbsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/netbsd-x64@0.19.11:
@@ -1881,7 +1796,6 @@ packages:
cpu: [x64]
os: [netbsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/netbsd-x64@0.21.2:
@@ -1899,7 +1813,6 @@ packages:
cpu: [x64]
os: [netbsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/openbsd-x64@0.17.6:
@@ -1908,7 +1821,6 @@ packages:
cpu: [x64]
os: [openbsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/openbsd-x64@0.19.11:
@@ -1917,7 +1829,6 @@ packages:
cpu: [x64]
os: [openbsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/openbsd-x64@0.21.2:
@@ -1935,7 +1846,6 @@ packages:
cpu: [x64]
os: [openbsd]
requiresBuild: true
- dev: true
optional: true
/@esbuild/sunos-x64@0.17.6:
@@ -1944,7 +1854,6 @@ packages:
cpu: [x64]
os: [sunos]
requiresBuild: true
- dev: true
optional: true
/@esbuild/sunos-x64@0.19.11:
@@ -1953,7 +1862,6 @@ packages:
cpu: [x64]
os: [sunos]
requiresBuild: true
- dev: true
optional: true
/@esbuild/sunos-x64@0.21.2:
@@ -1971,7 +1879,6 @@ packages:
cpu: [x64]
os: [sunos]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-arm64@0.17.6:
@@ -1980,7 +1887,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-arm64@0.19.11:
@@ -1989,7 +1895,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-arm64@0.21.2:
@@ -2007,7 +1912,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-ia32@0.17.6:
@@ -2016,7 +1920,6 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-ia32@0.19.11:
@@ -2025,7 +1928,6 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-ia32@0.21.2:
@@ -2043,7 +1945,6 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-x64@0.17.6:
@@ -2052,7 +1953,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-x64@0.19.11:
@@ -2061,7 +1961,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@esbuild/win32-x64@0.21.2:
@@ -2079,7 +1978,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@eslint-community/eslint-utils@4.4.0(eslint@8.57.0):
@@ -2222,7 +2120,6 @@ packages:
strip-ansi-cjs: /strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: /wrap-ansi@7.0.0
- dev: true
/@jest/schemas@29.6.3:
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
@@ -2249,39 +2146,32 @@ packages:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
- dev: true
/@jridgewell/resolve-uri@3.1.2:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
- dev: true
/@jridgewell/set-array@1.2.1:
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
- dev: true
/@jridgewell/sourcemap-codec@1.5.0:
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
- dev: true
/@jridgewell/trace-mapping@0.3.25:
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
- dev: true
/@jridgewell/trace-mapping@0.3.9:
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
- dev: true
/@jspm/core@2.0.1:
resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==}
- dev: true
/@lukeed/ms@2.0.2:
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
@@ -2328,7 +2218,6 @@ packages:
vfile: 5.3.7
transitivePeerDependencies:
- supports-color
- dev: true
/@netlify/binary-info@1.0.0:
resolution: {integrity: sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw==}
@@ -2858,7 +2747,6 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
semver: 7.6.3
- dev: true
/@npmcli/git@4.1.0:
resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==}
@@ -2874,7 +2762,6 @@ packages:
which: 3.0.1
transitivePeerDependencies:
- bluebird
- dev: true
/@npmcli/package-json@4.0.1:
resolution: {integrity: sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==}
@@ -2889,14 +2776,12 @@ packages:
semver: 7.6.3
transitivePeerDependencies:
- bluebird
- dev: true
/@npmcli/promise-spawn@6.0.2:
resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
which: 3.0.1
- dev: true
/@octokit/auth-token@4.0.0:
resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==}
@@ -3151,7 +3036,6 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
requiresBuild: true
- dev: true
optional: true
/@playwright/test@1.44.1:
@@ -3280,7 +3164,6 @@ packages:
- terser
- ts-node
- utf-8-validate
- dev: true
/@remix-run/eslint-config@2.11.1(eslint@8.57.0)(react@18.3.1)(typescript@5.4.5):
resolution: {integrity: sha512-Qzrww4D1Bfl+Z+qYprOc43/iEl/QS1i+kTo0XickLMEiTsal0qEeIt9Zod6alO0DvLEm6ixUPQDHKcn2trwLtA==}
@@ -3464,7 +3347,6 @@ packages:
cpu: [arm]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-android-arm64@4.18.0:
@@ -3480,7 +3362,6 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-darwin-arm64@4.18.0:
@@ -3496,7 +3377,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-darwin-x64@4.18.0:
@@ -3512,7 +3392,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.18.0:
@@ -3528,7 +3407,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-arm-musleabihf@4.18.0:
@@ -3544,7 +3422,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-arm64-gnu@4.18.0:
@@ -3560,7 +3437,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-arm64-musl@4.18.0:
@@ -3576,7 +3452,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-powerpc64le-gnu@4.18.0:
@@ -3592,7 +3467,6 @@ packages:
cpu: [ppc64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-riscv64-gnu@4.18.0:
@@ -3608,7 +3482,6 @@ packages:
cpu: [riscv64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-s390x-gnu@4.18.0:
@@ -3624,7 +3497,6 @@ packages:
cpu: [s390x]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-x64-gnu@4.18.0:
@@ -3640,7 +3512,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-linux-x64-musl@4.18.0:
@@ -3656,7 +3527,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-win32-arm64-msvc@4.18.0:
@@ -3672,7 +3542,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-win32-ia32-msvc@4.18.0:
@@ -3688,7 +3557,6 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@rollup/rollup-win32-x64-msvc@4.18.0:
@@ -3704,7 +3572,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
- dev: true
optional: true
/@rushstack/eslint-patch@1.10.4:
@@ -3767,25 +3634,20 @@ packages:
/@tsconfig/node10@1.0.11:
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
- dev: true
/@tsconfig/node12@1.0.11:
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
- dev: true
/@tsconfig/node14@1.0.3:
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
- dev: true
/@tsconfig/node16@1.0.4:
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
- dev: true
/@types/acorn@4.0.6:
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
dependencies:
'@types/estree': 1.0.5
- dev: true
/@types/aria-query@5.0.4:
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@@ -3804,23 +3666,19 @@ packages:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies:
'@types/ms': 0.7.34
- dev: true
/@types/estree-jsx@1.0.5:
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
dependencies:
'@types/estree': 1.0.5
- dev: true
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
- dev: true
/@types/hast@2.3.10:
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
dependencies:
'@types/unist': 2.0.10
- dev: true
/@types/http-cache-semantics@4.0.4:
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
@@ -3860,11 +3718,9 @@ packages:
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
dependencies:
'@types/unist': 2.0.10
- dev: true
/@types/mdx@2.0.13:
resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
- dev: true
/@types/minimist@1.2.5:
resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
@@ -3872,13 +3728,11 @@ packages:
/@types/ms@0.7.34:
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
- dev: true
/@types/node@20.16.3:
resolution: {integrity: sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==}
dependencies:
undici-types: 6.19.8
- dev: true
/@types/node@20.5.1:
resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==}
@@ -3888,7 +3742,6 @@ packages:
resolution: {integrity: sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==}
dependencies:
undici-types: 6.19.8
- dev: true
/@types/normalize-package-data@2.4.4:
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -3925,7 +3778,6 @@ packages:
/@types/unist@2.0.10:
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
- dev: true
/@types/yargs-parser@21.0.3:
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
@@ -4303,7 +4155,6 @@ packages:
'@babel/core': 7.25.2
transitivePeerDependencies:
- supports-color
- dev: true
/@vanilla-extract/css@1.15.3:
resolution: {integrity: sha512-mxoskDAxdQAspbkmQRxBvolUi1u1jnyy9WZGm+GeH8V2wwhEvndzl1QoK7w8JfA0WFevTxbev5d+i+xACZlPhA==}
@@ -4321,7 +4172,6 @@ packages:
picocolors: 1.0.1
transitivePeerDependencies:
- babel-plugin-macros
- dev: true
/@vanilla-extract/integration@6.5.0(@types/node@20.16.3):
resolution: {integrity: sha512-E2YcfO8vA+vs+ua+gpvy1HRqvgWbI+MTlUpxA8FvatOvybuNcWAY0CKwQ/Gpj7rswYKtC6C7+xw33emM6/ImdQ==}
@@ -4350,11 +4200,9 @@ packages:
- sugarss
- supports-color
- terser
- dev: true
/@vanilla-extract/private@1.0.5:
resolution: {integrity: sha512-6YXeOEKYTA3UV+RC8DeAjFk+/okoNz/h88R+McnzA2zpaVqTR/Ep+vszkWYlGBcMNO7vEkqbq5nT/JMMvhi+tw==}
- dev: true
/@vercel/nft@0.27.1(supports-color@9.4.0):
resolution: {integrity: sha512-K6upzYHCV1cq2gP83r1o8uNV1vwvAlozvMqp7CEjYWxo0CMI8/4jKcDkVjlypVhrfZ54SXwh9QbH0ZIk/vQCsw==}
@@ -4550,12 +4398,10 @@ packages:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
acorn: 8.12.1
- dev: true
/acorn-walk@8.3.2:
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
engines: {node: '>=0.4.0'}
- dev: true
/acorn@7.4.1:
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
@@ -4567,7 +4413,6 @@ packages:
resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
engines: {node: '>=0.4.0'}
hasBin: true
- dev: true
/agent-base@6.0.2(supports-color@9.4.0):
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
@@ -4593,7 +4438,6 @@ packages:
dependencies:
clean-stack: 2.2.0
indent-string: 4.0.0
- dev: true
/aggregate-error@4.0.1:
resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
@@ -4715,26 +4559,22 @@ packages:
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
- dev: true
/ansi-regex@6.0.1:
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
engines: {node: '>=12'}
- dev: true
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
dependencies:
color-convert: 1.9.3
- dev: true
/ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
dependencies:
color-convert: 2.0.1
- dev: true
/ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
@@ -4744,7 +4584,6 @@ packages:
/ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
- dev: true
/ansi-to-html@0.7.2:
resolution: {integrity: sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==}
@@ -4815,11 +4654,9 @@ packages:
/arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
- dev: true
/arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
- dev: true
/argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@@ -4829,7 +4666,6 @@ packages:
/argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
- dev: true
/aria-query@5.1.3:
resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
@@ -4992,7 +4828,6 @@ packages:
/astring@1.8.6:
resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==}
hasBin: true
- dev: true
/async-sema@3.1.1:
resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==}
@@ -5063,11 +4898,9 @@ packages:
/bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
- dev: true
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
- dev: true
/bare-events@2.4.2:
resolution: {integrity: sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==}
@@ -5109,7 +4942,6 @@ packages:
/base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
- dev: true
/basic-auth@2.0.1:
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
@@ -5158,7 +4990,6 @@ packages:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
- dev: true
/blueimp-md5@2.19.0:
resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==}
@@ -5212,7 +5043,6 @@ packages:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
- dev: true
/braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -5224,7 +5054,6 @@ packages:
resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==}
dependencies:
pako: 0.2.9
- dev: true
/browserslist@4.23.3:
resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==}
@@ -5235,7 +5064,6 @@ packages:
electron-to-chromium: 1.5.6
node-releases: 2.0.18
update-browserslist-db: 1.1.0(browserslist@4.23.3)
- dev: true
/buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
@@ -5258,7 +5086,6 @@ packages:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
- dev: true
/buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
@@ -5304,7 +5131,6 @@ packages:
/cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
- dev: true
/cacache@17.1.4:
resolution: {integrity: sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==}
@@ -5322,7 +5148,6 @@ packages:
ssri: 10.0.6
tar: 6.2.1
unique-filename: 3.0.0
- dev: true
/cacheable-lookup@7.0.0:
resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==}
@@ -5392,11 +5217,9 @@ packages:
/caniuse-lite@1.0.30001651:
resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==}
- dev: true
/ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
- dev: true
/chai@4.4.1:
resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
@@ -5418,7 +5241,6 @@ packages:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
- dev: true
/chalk@3.0.0:
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
@@ -5434,7 +5256,6 @@ packages:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
- dev: true
/chalk@5.3.0:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
@@ -5443,7 +5264,6 @@ packages:
/character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
- dev: true
/character-entities-legacy@1.1.4:
resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
@@ -5451,7 +5271,6 @@ packages:
/character-entities-legacy@3.0.0:
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
- dev: true
/character-entities@1.2.4:
resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==}
@@ -5459,7 +5278,6 @@ packages:
/character-entities@2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
- dev: true
/character-reference-invalid@1.1.4:
resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
@@ -5467,7 +5285,6 @@ packages:
/character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
- dev: true
/chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
@@ -5495,12 +5312,10 @@ packages:
/chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
- dev: true
/chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
- dev: true
/ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
@@ -5537,7 +5352,6 @@ packages:
/clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
- dev: true
/clean-stack@4.2.0:
resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==}
@@ -5563,7 +5377,6 @@ packages:
engines: {node: '>=8'}
dependencies:
restore-cursor: 3.1.0
- dev: true
/cli-cursor@4.0.0:
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
@@ -5589,7 +5402,6 @@ packages:
/cli-spinners@2.9.2:
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
engines: {node: '>=6'}
- dev: true
/cli-truncate@4.0.0:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
@@ -5638,28 +5450,23 @@ packages:
/clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
- dev: true
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
color-name: 1.1.3
- dev: true
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
dependencies:
color-name: 1.1.4
- dev: true
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
- dev: true
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- dev: true
/color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
@@ -5724,7 +5531,6 @@ packages:
/comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
- dev: true
/commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
@@ -5828,7 +5634,6 @@ packages:
/confbox@0.1.7:
resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==}
- dev: true
/config-chain@1.1.13:
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
@@ -5912,7 +5717,6 @@ packages:
/convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
- dev: true
/cookie-es@1.1.0:
resolution: {integrity: sha512-L2rLOcK0wzWSfSDA33YR+PUHDG10a8px7rUHKWbGLP4YfbsMed2KFUw5fczvDPbT98DDe3LEzviswl810apTEw==}
@@ -5931,7 +5735,6 @@ packages:
/core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
- dev: true
/cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6)(ts-node@10.9.2)(typescript@5.4.5):
resolution: {integrity: sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==}
@@ -6050,7 +5853,6 @@ packages:
/create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
- dev: true
/cron-parser@4.9.0:
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
@@ -6130,13 +5932,11 @@ packages:
/css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
- dev: true
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
- dev: true
/cssfilter@0.0.10:
resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==}
@@ -6151,7 +5951,6 @@ packages:
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
- dev: true
/cyclist@1.0.2:
resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==}
@@ -6270,7 +6069,6 @@ packages:
dependencies:
ms: 2.1.2
supports-color: 9.4.0
- dev: true
/decache@4.6.2:
resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==}
@@ -6295,7 +6093,6 @@ packages:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
dependencies:
character-entities: 2.0.2
- dev: true
/decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
@@ -6311,7 +6108,6 @@ packages:
peerDependenciesMeta:
babel-plugin-macros:
optional: true
- dev: true
/deep-eql@4.1.3:
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
@@ -6355,18 +6151,15 @@ packages:
/deep-object-diff@1.1.9:
resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==}
- dev: true
/deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
- dev: true
/defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
dependencies:
clone: 1.0.4
- dev: true
/defer-to-connect@2.0.1:
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
@@ -6419,7 +6212,6 @@ packages:
/dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
- dev: true
/destr@2.0.3:
resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==}
@@ -6516,12 +6308,10 @@ packages:
/diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
- dev: true
/diff@5.2.0:
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
engines: {node: '>=0.3.1'}
- dev: true
/dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
@@ -6610,7 +6400,6 @@ packages:
/dotenv@16.4.5:
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'}
- dev: true
/dotenv@8.6.0:
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
@@ -6624,11 +6413,9 @@ packages:
inherits: 2.0.4
readable-stream: 2.3.8
stream-shift: 1.0.3
- dev: true
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- dev: true
/ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
@@ -6641,7 +6428,6 @@ packages:
/electron-to-chromium@1.5.6:
resolution: {integrity: sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==}
- dev: true
/emoji-regex@10.4.0:
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
@@ -6653,11 +6439,9 @@ packages:
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
- dev: true
/emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
- dev: true
/enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
@@ -6671,7 +6455,6 @@ packages:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies:
once: 1.4.0
- dev: true
/enhance-visitors@1.0.0:
resolution: {integrity: sha512-+29eJLiUixTEDRaZ35Vu8jP3gPLNcQQkQkOQjLp2X+6cZGGPDD/uasbFzvLsJKnGZnvmyZ0srxudwOtskHeIDA==}
@@ -6720,7 +6503,6 @@ packages:
/err-code@2.0.3:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
- dev: true
/error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@@ -6832,7 +6614,6 @@ packages:
/es-module-lexer@1.5.4:
resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==}
- dev: true
/es-object-atoms@1.0.0:
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
@@ -6879,7 +6660,6 @@ packages:
esbuild: 0.17.6
local-pkg: 0.5.0
resolve.exports: 2.0.2
- dev: true
/esbuild@0.17.6:
resolution: {integrity: sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==}
@@ -6909,7 +6689,6 @@ packages:
'@esbuild/win32-arm64': 0.17.6
'@esbuild/win32-ia32': 0.17.6
'@esbuild/win32-x64': 0.17.6
- dev: true
/esbuild@0.19.11:
resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==}
@@ -6940,7 +6719,6 @@ packages:
'@esbuild/win32-arm64': 0.19.11
'@esbuild/win32-ia32': 0.19.11
'@esbuild/win32-x64': 0.19.11
- dev: true
/esbuild@0.21.2:
resolution: {integrity: sha512-LmHPAa5h4tSxz+g/D8IHY6wCjtIiFx8I7/Q0Aq+NmvtoYvyMnJU0KQJcqB6QH30X9x/W4CemgUtPgQDZFca5SA==}
@@ -7002,12 +6780,10 @@ packages:
'@esbuild/win32-arm64': 0.21.5
'@esbuild/win32-ia32': 0.21.5
'@esbuild/win32-x64': 0.21.5
- dev: true
/escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
- dev: true
/escape-goat@4.0.0:
resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==}
@@ -7020,7 +6796,6 @@ packages:
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
- dev: true
/escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
@@ -7371,7 +7146,7 @@ packages:
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
hasown: 2.0.2
- is-core-module: 2.15.0
+ is-core-module: 2.13.1
is-glob: 4.0.3
minimatch: 3.1.2
object.fromentries: 2.0.8
@@ -7904,7 +7679,6 @@ packages:
resolution: {integrity: sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==}
dependencies:
'@types/estree': 1.0.5
- dev: true
/estree-util-build-jsx@2.2.2:
resolution: {integrity: sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==}
@@ -7912,15 +7686,12 @@ packages:
'@types/estree-jsx': 1.0.5
estree-util-is-identifier-name: 2.1.0
estree-walker: 3.0.3
- dev: true
/estree-util-is-identifier-name@1.1.0:
resolution: {integrity: sha512-OVJZ3fGGt9By77Ix9NhaRbzfbDV/2rx9EP7YIDJTmsZSEc5kYn2vWcNccYyahJL2uAQZK2a5Or2i0wtIKTPoRQ==}
- dev: true
/estree-util-is-identifier-name@2.1.0:
resolution: {integrity: sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==}
- dev: true
/estree-util-to-js@1.2.0:
resolution: {integrity: sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==}
@@ -7928,21 +7699,18 @@ packages:
'@types/estree-jsx': 1.0.5
astring: 1.8.6
source-map: 0.7.4
- dev: true
/estree-util-value-to-estree@1.3.0:
resolution: {integrity: sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==}
engines: {node: '>=12.0.0'}
dependencies:
is-plain-obj: 3.0.0
- dev: true
/estree-util-visit@1.2.1:
resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==}
dependencies:
'@types/estree-jsx': 1.0.5
'@types/unist': 2.0.10
- dev: true
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -7952,7 +7720,6 @@ packages:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
dependencies:
'@types/estree': 1.0.5
- dev: true
/esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@@ -7969,7 +7736,6 @@ packages:
dependencies:
'@types/node': 22.5.2
require-like: 0.1.2
- dev: true
/event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
@@ -8001,7 +7767,6 @@ packages:
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
- dev: true
/execa@6.1.0:
resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==}
@@ -8036,7 +7801,6 @@ packages:
/exit-hook@2.2.1:
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
engines: {node: '>=6'}
- dev: true
/expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
@@ -8105,7 +7869,6 @@ packages:
/extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
- dev: true
/external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
@@ -8250,7 +8013,6 @@ packages:
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
dependencies:
format: 0.2.2
- dev: true
/fd-slicer@1.1.0:
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
@@ -8420,7 +8182,6 @@ packages:
dependencies:
locate-path: 6.0.0
path-exists: 4.0.0
- dev: true
/find-up@6.3.0:
resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
@@ -8505,7 +8266,6 @@ packages:
dependencies:
cross-spawn: 7.0.3
signal-exit: 4.1.0
- dev: true
/form-data-encoder@2.1.4:
resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==}
@@ -8515,7 +8275,6 @@ packages:
/format@0.2.2:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
- dev: true
/formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
@@ -8547,7 +8306,6 @@ packages:
/fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
- dev: true
/fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
@@ -8556,7 +8314,6 @@ packages:
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.1
- dev: true
/fs-extra@11.2.0:
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
@@ -8581,14 +8338,12 @@ packages:
engines: {node: '>= 8'}
dependencies:
minipass: 3.3.6
- dev: true
/fs-minipass@3.0.3:
resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
minipass: 7.1.2
- dev: true
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -8655,12 +8410,10 @@ packages:
resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==}
dependencies:
loader-utils: 3.3.1
- dev: true
/gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
- dev: true
/get-amd-module-type@5.0.1:
resolution: {integrity: sha512-jb65zDeHyDjFR1loOVk0HQGM5WNwoGB8aLWy3LKCieMKol0/ProHkhO2X1JxojuN10vbz1qNn09MJ7tNp7qMzw==}
@@ -8722,7 +8475,6 @@ packages:
/get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
- dev: true
/get-stream@8.0.1:
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
@@ -8813,7 +8565,6 @@ packages:
minipass: 7.1.2
package-json-from-dist: 1.0.0
path-scurry: 1.11.1
- dev: true
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
@@ -8871,7 +8622,6 @@ packages:
/globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
- dev: true
/globals@12.4.0:
resolution: {integrity: sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==}
@@ -8958,7 +8708,6 @@ packages:
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- dev: true
/graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@@ -8974,7 +8723,6 @@ packages:
peek-stream: 1.1.3
pumpify: 1.5.1
through2: 2.0.5
- dev: true
/h3@1.11.1:
resolution: {integrity: sha512-AbaH6IDnZN6nmbnJOH72y3c5Wwh9P97soSVdGSBbcDACRdkC0FEWf25pzx4f/NuOCK6quHmW18yF2Wx+G4Zi1A==}
@@ -9005,12 +8753,10 @@ packages:
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
- dev: true
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
- dev: true
/has-own-prop@2.0.0:
resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==}
@@ -9081,11 +8827,9 @@ packages:
zwitch: 2.0.4
transitivePeerDependencies:
- supports-color
- dev: true
/hast-util-whitespace@2.0.1:
resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==}
- dev: true
/hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
@@ -9103,7 +8847,6 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
lru-cache: 7.18.3
- dev: true
/hosted-git-info@7.0.2:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
@@ -9218,7 +8961,6 @@ packages:
/human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
- dev: true
/human-signals@3.0.1:
resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==}
@@ -9255,11 +8997,9 @@ packages:
postcss: ^8.1.0
dependencies:
postcss: 8.4.41
- dev: true
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
- dev: true
/ignore@4.0.6:
resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
@@ -9300,12 +9040,10 @@ packages:
/imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
- dev: true
/indent-string@4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
- dev: true
/indent-string@5.0.0:
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
@@ -9348,7 +9086,6 @@ packages:
/inline-style-parser@0.1.1:
resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
- dev: true
/inquirer-autocomplete-prompt@1.4.0(inquirer@6.5.2):
resolution: {integrity: sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==}
@@ -9468,7 +9205,6 @@ packages:
/is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
- dev: true
/is-alphanumerical@1.0.4:
resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==}
@@ -9482,7 +9218,6 @@ packages:
dependencies:
is-alphabetical: 2.0.1
is-decimal: 2.0.1
- dev: true
/is-arguments@1.1.1:
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
@@ -9537,7 +9272,6 @@ packages:
/is-buffer@2.0.5:
resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
engines: {node: '>=4'}
- dev: true
/is-builtin-module@3.2.1:
resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
@@ -9568,7 +9302,6 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
hasown: 2.0.2
- dev: true
/is-data-view@1.0.1:
resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==}
@@ -9590,11 +9323,9 @@ packages:
/is-decimal@2.0.1:
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
- dev: true
/is-deflate@1.0.0:
resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==}
- dev: true
/is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
@@ -9626,7 +9357,6 @@ packages:
/is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
- dev: true
/is-fullwidth-code-point@4.0.0:
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
@@ -9655,7 +9385,6 @@ packages:
/is-gzip@1.0.0:
resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==}
engines: {node: '>=0.10.0'}
- dev: true
/is-hexadecimal@1.0.4:
resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
@@ -9663,7 +9392,6 @@ packages:
/is-hexadecimal@2.0.1:
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
- dev: true
/is-in-ci@0.1.0:
resolution: {integrity: sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==}
@@ -9690,7 +9418,6 @@ packages:
/is-interactive@1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
- dev: true
/is-interactive@2.0.0:
resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
@@ -9751,12 +9478,10 @@ packages:
/is-plain-obj@3.0.0:
resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==}
engines: {node: '>=10'}
- dev: true
/is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
- dev: true
/is-plain-object@2.0.4:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
@@ -9769,7 +9494,6 @@ packages:
resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==}
dependencies:
'@types/estree': 1.0.5
- dev: true
/is-regex@1.1.4:
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
@@ -9794,7 +9518,6 @@ packages:
/is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
- dev: true
/is-stream@3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
@@ -9847,7 +9570,6 @@ packages:
/is-unicode-supported@0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
- dev: true
/is-unicode-supported@1.3.0:
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
@@ -9910,7 +9632,6 @@ packages:
/isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
- dev: true
/isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@@ -9954,11 +9675,9 @@ packages:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
- dev: true
/javascript-stringify@2.1.0:
resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==}
- dev: true
/jest-get-type@27.5.1:
resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==}
@@ -10012,19 +9731,16 @@ packages:
hasBin: true
dependencies:
argparse: 2.0.1
- dev: true
/jsesc@2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'}
hasBin: true
- dev: true
/jsesc@3.0.2:
resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
engines: {node: '>=6'}
hasBin: true
- dev: true
/json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -10041,7 +9757,6 @@ packages:
/json-parse-even-better-errors@3.0.2:
resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- dev: true
/json-schema-ref-resolver@1.0.1:
resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
@@ -10072,7 +9787,6 @@ packages:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
- dev: true
/jsonc-parser@3.2.1:
resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
@@ -10090,7 +9804,6 @@ packages:
universalify: 2.0.1
optionalDependencies:
graceful-fs: 4.2.11
- dev: true
/jsonparse@1.3.1:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
@@ -10178,7 +9891,6 @@ packages:
/kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
- dev: true
/kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
@@ -10280,7 +9992,6 @@ packages:
/lilconfig@3.1.2:
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
engines: {node: '>=14'}
- dev: true
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -10373,7 +10084,6 @@ packages:
/loader-utils@3.3.1:
resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==}
engines: {node: '>= 12.13.0'}
- dev: true
/local-pkg@0.5.0:
resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
@@ -10381,7 +10091,6 @@ packages:
dependencies:
mlly: 1.7.1
pkg-types: 1.1.3
- dev: true
/locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
@@ -10395,7 +10104,6 @@ packages:
engines: {node: '>=10'}
dependencies:
p-locate: 5.0.0
- dev: true
/locate-path@7.2.0:
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
@@ -10410,11 +10118,9 @@ packages:
/lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
- dev: true
/lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
- dev: true
/lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
@@ -10498,7 +10204,6 @@ packages:
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
- dev: true
/log-process-errors@8.0.0:
resolution: {integrity: sha512-+SNGqNC1gCMJfhwYzAHr/YgNT/ZJc+V2nCkvtPnjrENMeCe+B/jgShBW0lmWoh6uVV2edFAPc/IUOkDdsjTbTg==}
@@ -10526,7 +10231,6 @@ packages:
dependencies:
chalk: 4.1.2
is-unicode-supported: 0.1.0
- dev: true
/log-symbols@6.0.0:
resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
@@ -10572,7 +10276,6 @@ packages:
/longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
- dev: true
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
@@ -10593,13 +10296,11 @@ packages:
/lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
- dev: true
/lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies:
yallist: 3.1.1
- dev: true
/lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
@@ -10611,7 +10312,6 @@ packages:
/lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
- dev: true
/luxon@3.4.4:
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
@@ -10650,7 +10350,6 @@ packages:
/make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
- dev: true
/map-obj@1.0.1:
resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
@@ -10670,7 +10369,6 @@ packages:
/markdown-extensions@1.1.1:
resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==}
engines: {node: '>=0.10.0'}
- dev: true
/maxstache-stream@1.0.4:
resolution: {integrity: sha512-v8qlfPN0pSp7bdSoLo1NTjG43GXGqk5W2NWFnOCq2GlmFFqebGzPCjLKSbShuqIOVorOtZSAy7O/S1OCCRONUw==}
@@ -10698,7 +10396,6 @@ packages:
'@types/mdast': 3.0.15
'@types/unist': 2.0.10
unist-util-visit: 4.1.2
- dev: true
/mdast-util-from-markdown@0.8.5:
resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==}
@@ -10729,7 +10426,6 @@ packages:
uvu: 0.5.6
transitivePeerDependencies:
- supports-color
- dev: true
/mdast-util-frontmatter@1.0.1:
resolution: {integrity: sha512-JjA2OjxRqAa8wEG8hloD0uTU0kdn8kbtOWpPP94NBkfAlbxn4S8gCGf/9DwFtEeGPXrDcNXdiDjVaRdUFqYokw==}
@@ -10737,7 +10433,6 @@ packages:
'@types/mdast': 3.0.15
mdast-util-to-markdown: 1.5.0
micromark-extension-frontmatter: 1.1.1
- dev: true
/mdast-util-mdx-expression@1.3.2:
resolution: {integrity: sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==}
@@ -10749,7 +10444,6 @@ packages:
mdast-util-to-markdown: 1.5.0
transitivePeerDependencies:
- supports-color
- dev: true
/mdast-util-mdx-jsx@2.1.4:
resolution: {integrity: sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==}
@@ -10768,7 +10462,6 @@ packages:
vfile-message: 3.1.4
transitivePeerDependencies:
- supports-color
- dev: true
/mdast-util-mdx@2.0.1:
resolution: {integrity: sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==}
@@ -10780,7 +10473,6 @@ packages:
mdast-util-to-markdown: 1.5.0
transitivePeerDependencies:
- supports-color
- dev: true
/mdast-util-mdxjs-esm@1.3.1:
resolution: {integrity: sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==}
@@ -10792,14 +10484,12 @@ packages:
mdast-util-to-markdown: 1.5.0
transitivePeerDependencies:
- supports-color
- dev: true
/mdast-util-phrasing@3.0.1:
resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==}
dependencies:
'@types/mdast': 3.0.15
unist-util-is: 5.2.1
- dev: true
/mdast-util-to-hast@12.3.0:
resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==}
@@ -10812,7 +10502,6 @@ packages:
unist-util-generated: 2.0.1
unist-util-position: 4.0.4
unist-util-visit: 4.1.2
- dev: true
/mdast-util-to-markdown@1.5.0:
resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==}
@@ -10825,7 +10514,6 @@ packages:
micromark-util-decode-string: 1.1.0
unist-util-visit: 4.1.2
zwitch: 2.0.4
- dev: true
/mdast-util-to-string@2.0.0:
resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==}
@@ -10835,7 +10523,6 @@ packages:
resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==}
dependencies:
'@types/mdast': 3.0.15
- dev: true
/mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
@@ -10849,7 +10536,6 @@ packages:
resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==}
dependencies:
'@babel/runtime': 7.25.0
- dev: true
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
@@ -10898,7 +10584,6 @@ packages:
/merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
- dev: true
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
@@ -10940,7 +10625,6 @@ packages:
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
- dev: true
/micromark-extension-frontmatter@1.1.1:
resolution: {integrity: sha512-m2UH9a7n3W8VAH9JO9y01APpPKmNNNs71P0RbknEmYSaZU5Ghogv38BYO94AI5Xw6OYfxZRdHZZ2nYjs/Z+SZQ==}
@@ -10949,7 +10633,6 @@ packages:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
- dev: true
/micromark-extension-mdx-expression@1.0.8:
resolution: {integrity: sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==}
@@ -10962,7 +10645,6 @@ packages:
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
- dev: true
/micromark-extension-mdx-jsx@1.0.5:
resolution: {integrity: sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==}
@@ -10977,13 +10659,11 @@ packages:
micromark-util-types: 1.1.0
uvu: 0.5.6
vfile-message: 3.1.4
- dev: true
/micromark-extension-mdx-md@1.0.1:
resolution: {integrity: sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==}
dependencies:
micromark-util-types: 1.1.0
- dev: true
/micromark-extension-mdxjs-esm@1.0.5:
resolution: {integrity: sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==}
@@ -10997,7 +10677,6 @@ packages:
unist-util-position-from-estree: 1.1.2
uvu: 0.5.6
vfile-message: 3.1.4
- dev: true
/micromark-extension-mdxjs@1.0.1:
resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==}
@@ -11010,7 +10689,6 @@ packages:
micromark-extension-mdxjs-esm: 1.0.5
micromark-util-combine-extensions: 1.1.0
micromark-util-types: 1.1.0
- dev: true
/micromark-factory-destination@1.1.0:
resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==}
@@ -11018,7 +10696,6 @@ packages:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
- dev: true
/micromark-factory-label@1.1.0:
resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==}
@@ -11027,7 +10704,6 @@ packages:
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
- dev: true
/micromark-factory-mdx-expression@1.0.9:
resolution: {integrity: sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==}
@@ -11040,14 +10716,12 @@ packages:
unist-util-position-from-estree: 1.1.2
uvu: 0.5.6
vfile-message: 3.1.4
- dev: true
/micromark-factory-space@1.1.0:
resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==}
dependencies:
micromark-util-character: 1.2.0
micromark-util-types: 1.1.0
- dev: true
/micromark-factory-title@1.1.0:
resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==}
@@ -11056,7 +10730,6 @@ packages:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
- dev: true
/micromark-factory-whitespace@1.1.0:
resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==}
@@ -11065,20 +10738,17 @@ packages:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
- dev: true
/micromark-util-character@1.2.0:
resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==}
dependencies:
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
- dev: true
/micromark-util-chunked@1.1.0:
resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==}
dependencies:
micromark-util-symbol: 1.1.0
- dev: true
/micromark-util-classify-character@1.1.0:
resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==}
@@ -11086,20 +10756,17 @@ packages:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
- dev: true
/micromark-util-combine-extensions@1.1.0:
resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==}
dependencies:
micromark-util-chunked: 1.1.0
micromark-util-types: 1.1.0
- dev: true
/micromark-util-decode-numeric-character-reference@1.1.0:
resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==}
dependencies:
micromark-util-symbol: 1.1.0
- dev: true
/micromark-util-decode-string@1.1.0:
resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==}
@@ -11108,11 +10775,9 @@ packages:
micromark-util-character: 1.2.0
micromark-util-decode-numeric-character-reference: 1.1.0
micromark-util-symbol: 1.1.0
- dev: true
/micromark-util-encode@1.1.0:
resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==}
- dev: true
/micromark-util-events-to-acorn@1.2.3:
resolution: {integrity: sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==}
@@ -11125,23 +10790,19 @@ packages:
micromark-util-types: 1.1.0
uvu: 0.5.6
vfile-message: 3.1.4
- dev: true
/micromark-util-html-tag-name@1.2.0:
resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==}
- dev: true
/micromark-util-normalize-identifier@1.1.0:
resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==}
dependencies:
micromark-util-symbol: 1.1.0
- dev: true
/micromark-util-resolve-all@1.1.0:
resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==}
dependencies:
micromark-util-types: 1.1.0
- dev: true
/micromark-util-sanitize-uri@1.2.0:
resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==}
@@ -11149,7 +10810,6 @@ packages:
micromark-util-character: 1.2.0
micromark-util-encode: 1.1.0
micromark-util-symbol: 1.1.0
- dev: true
/micromark-util-subtokenize@1.1.0:
resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==}
@@ -11158,15 +10818,12 @@ packages:
micromark-util-symbol: 1.1.0
micromark-util-types: 1.1.0
uvu: 0.5.6
- dev: true
/micromark-util-symbol@1.1.0:
resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==}
- dev: true
/micromark-util-types@1.1.0:
resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==}
- dev: true
/micromark@2.11.4:
resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==}
@@ -11199,7 +10856,6 @@ packages:
uvu: 0.5.6
transitivePeerDependencies:
- supports-color
- dev: true
/micromatch@4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
@@ -11250,7 +10906,6 @@ packages:
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
- dev: true
/mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
@@ -11295,7 +10950,6 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
brace-expansion: 2.0.1
- dev: true
/minimist-options@4.1.0:
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
@@ -11308,45 +10962,38 @@ packages:
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
- dev: true
/minipass-collect@1.0.2:
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
engines: {node: '>= 8'}
dependencies:
minipass: 3.3.6
- dev: true
/minipass-flush@1.0.5:
resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
engines: {node: '>= 8'}
dependencies:
minipass: 3.3.6
- dev: true
/minipass-pipeline@1.2.4:
resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
engines: {node: '>=8'}
dependencies:
minipass: 3.3.6
- dev: true
/minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
dependencies:
yallist: 4.0.0
- dev: true
/minipass@5.0.0:
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
engines: {node: '>=8'}
- dev: true
/minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
- dev: true
/minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
@@ -11354,11 +11001,9 @@ packages:
dependencies:
minipass: 3.3.6
yallist: 4.0.0
- dev: true
/mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
- dev: true
/mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
@@ -11371,7 +11016,6 @@ packages:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
hasBin: true
- dev: true
/mlly@1.7.1:
resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==}
@@ -11380,11 +11024,9 @@ packages:
pathe: 1.1.2
pkg-types: 1.1.3
ufo: 1.5.4
- dev: true
/modern-ahocorasick@1.0.1:
resolution: {integrity: sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA==}
- dev: true
/module-definition@5.0.1:
resolution: {integrity: sha512-kvw3B4G19IXk+BOXnYq/D/VeO9qfHaapMeuS7w7sNUqmGaA6hywdFHMi+VWeR9wUScXM7XjoryTffCZ5B0/8IA==}
@@ -11424,7 +11066,6 @@ packages:
/mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
- dev: true
/mrmime@1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
@@ -11435,7 +11076,6 @@ packages:
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
- dev: true
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -11475,7 +11115,6 @@ packages:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
- dev: true
/napi-build-utils@1.0.2:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
@@ -11744,7 +11383,6 @@ packages:
/node-releases@2.0.18:
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
- dev: true
/node-source-walk@6.0.2:
resolution: {integrity: sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag==}
@@ -11814,7 +11452,6 @@ packages:
is-core-module: 2.15.0
semver: 7.6.3
validate-npm-package-license: 3.0.4
- dev: true
/normalize-package-data@6.0.2:
resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==}
@@ -11846,12 +11483,10 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
semver: 7.6.3
- dev: true
/npm-normalize-package-bin@3.0.1:
resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- dev: true
/npm-package-arg@10.1.0:
resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==}
@@ -11861,7 +11496,6 @@ packages:
proc-log: 3.0.0
semver: 7.6.3
validate-npm-package-name: 5.0.1
- dev: true
/npm-pick-manifest@8.0.2:
resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==}
@@ -11871,7 +11505,6 @@ packages:
npm-normalize-package-bin: 3.0.1
npm-package-arg: 10.1.0
semver: 7.6.3
- dev: true
/npm-run-all2@6.2.2:
resolution: {integrity: sha512-Q+alQAGIW7ZhKcxLt8GcSi3h3ryheD6xnmXahkMRVM5LYmajcUrSITm8h+OPC9RYWMV2GR0Q1ntTUCfxaNoOJw==}
@@ -11908,7 +11541,6 @@ packages:
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
- dev: true
/npm-run-path@5.3.0:
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
@@ -12043,7 +11675,6 @@ packages:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
- dev: true
/one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
@@ -12063,7 +11694,6 @@ packages:
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
- dev: true
/onetime@6.0.0:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
@@ -12139,7 +11769,6 @@ packages:
log-symbols: 4.1.0
strip-ansi: 6.0.1
wcwidth: 1.0.1
- dev: true
/ora@8.0.1:
resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==}
@@ -12171,7 +11800,6 @@ packages:
/outdent@0.8.0:
resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==}
- dev: true
/p-cancelable@3.0.0:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
@@ -12230,7 +11858,6 @@ packages:
engines: {node: '>=10'}
dependencies:
yocto-queue: 0.1.0
- dev: true
/p-limit@4.0.0:
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
@@ -12258,7 +11885,6 @@ packages:
engines: {node: '>=10'}
dependencies:
p-limit: 3.1.0
- dev: true
/p-locate@6.0.0:
resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
@@ -12277,7 +11903,6 @@ packages:
engines: {node: '>=10'}
dependencies:
aggregate-error: 3.1.0
- dev: true
/p-map@5.5.0:
resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==}
@@ -12347,7 +11972,6 @@ packages:
/package-json-from-dist@1.0.0:
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
- dev: true
/package-json@8.1.1:
resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==}
@@ -12361,7 +11985,6 @@ packages:
/pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
- dev: true
/parallel-transform@1.2.0:
resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==}
@@ -12400,7 +12023,6 @@ packages:
is-alphanumerical: 2.0.1
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
- dev: true
/parse-github-url@1.0.3:
resolution: {integrity: sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==}
@@ -12443,7 +12065,6 @@ packages:
/parse-ms@2.1.0:
resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
engines: {node: '>=6'}
- dev: true
/parse-ms@3.0.0:
resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==}
@@ -12457,7 +12078,6 @@ packages:
/path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
- dev: true
/path-exists@5.0.0:
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
@@ -12493,7 +12113,6 @@ packages:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.2
- dev: true
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
@@ -12524,7 +12143,6 @@ packages:
/pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
- dev: true
/pathval@1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
@@ -12541,7 +12159,6 @@ packages:
buffer-from: 1.1.2
duplexify: 3.7.1
through2: 2.0.5
- dev: true
/pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
@@ -12553,11 +12170,9 @@ packages:
'@types/estree': 1.0.5
estree-walker: 3.0.3
is-reference: 3.0.2
- dev: true
/picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
- dev: true
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
@@ -12573,7 +12188,6 @@ packages:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
engines: {node: '>=0.10'}
hasBin: true
- dev: true
/pify@3.0.0:
resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
@@ -12633,7 +12247,6 @@ packages:
confbox: 0.1.7
mlly: 1.7.1
pathe: 1.1.2
- dev: true
/playwright-core@1.44.1:
resolution: {integrity: sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==}
@@ -12667,7 +12280,6 @@ packages:
postcss: ^8.2.15
dependencies:
postcss: 8.4.41
- dev: true
/postcss-load-config@4.0.2(postcss@8.4.41)(ts-node@10.9.2):
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
@@ -12685,7 +12297,6 @@ packages:
postcss: 8.4.41
ts-node: 10.9.2(@types/node@20.16.3)(typescript@5.4.5)
yaml: 2.5.0
- dev: true
/postcss-modules-extract-imports@3.1.0(postcss@8.4.41):
resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
@@ -12694,7 +12305,6 @@ packages:
postcss: ^8.1.0
dependencies:
postcss: 8.4.41
- dev: true
/postcss-modules-local-by-default@4.0.5(postcss@8.4.41):
resolution: {integrity: sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==}
@@ -12706,7 +12316,6 @@ packages:
postcss: 8.4.41
postcss-selector-parser: 6.1.1
postcss-value-parser: 4.2.0
- dev: true
/postcss-modules-scope@3.2.0(postcss@8.4.41):
resolution: {integrity: sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==}
@@ -12716,7 +12325,6 @@ packages:
dependencies:
postcss: 8.4.41
postcss-selector-parser: 6.1.1
- dev: true
/postcss-modules-values@4.0.0(postcss@8.4.41):
resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==}
@@ -12726,7 +12334,6 @@ packages:
dependencies:
icss-utils: 5.1.0(postcss@8.4.41)
postcss: 8.4.41
- dev: true
/postcss-modules@6.0.0(postcss@8.4.41):
resolution: {integrity: sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ==}
@@ -12742,7 +12349,6 @@ packages:
postcss-modules-scope: 3.2.0(postcss@8.4.41)
postcss-modules-values: 4.0.0(postcss@8.4.41)
string-hash: 1.1.3
- dev: true
/postcss-selector-parser@6.1.1:
resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==}
@@ -12750,11 +12356,9 @@ packages:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
- dev: true
/postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
- dev: true
/postcss-values-parser@6.0.2(postcss@8.4.41):
resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==}
@@ -12775,7 +12379,6 @@ packages:
nanoid: 3.3.7
picocolors: 1.0.1
source-map-js: 1.2.0
- dev: true
/prebuild-install@7.1.2:
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
@@ -12842,7 +12445,6 @@ packages:
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
engines: {node: '>=10.13.0'}
hasBin: true
- dev: true
/prettier@3.3.3:
resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
@@ -12873,7 +12475,6 @@ packages:
engines: {node: '>=10'}
dependencies:
parse-ms: 2.1.0
- dev: true
/pretty-ms@8.0.0:
resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==}
@@ -12893,11 +12494,9 @@ packages:
/proc-log@3.0.0:
resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- dev: true
/process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
- dev: true
/process-warning@3.0.0:
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
@@ -12920,7 +12519,6 @@ packages:
peerDependenciesMeta:
bluebird:
optional: true
- dev: true
/promise-retry@2.0.1:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
@@ -12928,7 +12526,6 @@ packages:
dependencies:
err-code: 2.0.3
retry: 0.12.0
- dev: true
/prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@@ -12940,7 +12537,6 @@ packages:
/property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
- dev: true
/proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
@@ -12970,14 +12566,12 @@ packages:
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
- dev: true
/pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
- dev: true
/pumpify@1.5.1:
resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==}
@@ -12985,7 +12579,6 @@ packages:
duplexify: 3.7.1
inherits: 2.0.4
pump: 2.0.1
- dev: true
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
@@ -13095,7 +12688,6 @@ packages:
/react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
- dev: true
/react-router-dom@6.26.0(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==}
@@ -13209,7 +12801,6 @@ packages:
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
- dev: true
/readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
@@ -13218,7 +12809,6 @@ packages:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
- dev: true
/readable-stream@4.5.2:
resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==}
@@ -13278,7 +12868,6 @@ packages:
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
- dev: true
/regexp-tree@0.1.27:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
@@ -13326,7 +12915,6 @@ packages:
mdast-util-frontmatter: 1.0.1
micromark-extension-frontmatter: 1.1.1
unified: 10.1.2
- dev: true
/remark-mdx-frontmatter@1.1.1:
resolution: {integrity: sha512-7teX9DW4tI2WZkXS4DBxneYSY7NHiXl4AKdWDO9LXVweULlCT8OPWsOjLEnMIXViN1j+QcY8mfbq3k0EK6x3uA==}
@@ -13336,7 +12924,6 @@ packages:
estree-util-value-to-estree: 1.3.0
js-yaml: 4.1.0
toml: 3.0.0
- dev: true
/remark-mdx@2.3.0:
resolution: {integrity: sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==}
@@ -13345,7 +12932,6 @@ packages:
micromark-extension-mdxjs: 1.0.1
transitivePeerDependencies:
- supports-color
- dev: true
/remark-parse@10.0.2:
resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==}
@@ -13355,7 +12941,6 @@ packages:
unified: 10.1.2
transitivePeerDependencies:
- supports-color
- dev: true
/remark-rehype@10.1.0:
resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==}
@@ -13364,7 +12949,6 @@ packages:
'@types/mdast': 3.0.15
mdast-util-to-hast: 12.3.0
unified: 10.1.2
- dev: true
/remove-trailing-separator@1.1.0:
resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==}
@@ -13392,7 +12976,6 @@ packages:
/require-like@0.1.2:
resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==}
- dev: true
/require-package-name@2.0.1:
resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==}
@@ -13435,7 +13018,6 @@ packages:
/resolve.exports@2.0.2:
resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==}
engines: {node: '>=10'}
- dev: true
/resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
@@ -13476,7 +13058,6 @@ packages:
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
- dev: true
/restore-cursor@4.0.0:
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
@@ -13502,7 +13083,6 @@ packages:
/retry@0.12.0:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
- dev: true
/retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
@@ -13584,7 +13164,6 @@ packages:
'@rollup/rollup-win32-ia32-msvc': 4.20.0
'@rollup/rollup-win32-x64-msvc': 4.20.0
fsevents: 2.3.3
- dev: true
/run-async@2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
@@ -13609,7 +13188,6 @@ packages:
engines: {node: '>=6'}
dependencies:
mri: 1.2.0
- dev: true
/safe-array-concat@1.1.2:
resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==}
@@ -13691,7 +13269,6 @@ packages:
/semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
- dev: true
/semver@7.5.4:
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
@@ -13705,7 +13282,6 @@ packages:
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
engines: {node: '>=10'}
hasBin: true
- dev: true
/send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
@@ -13832,12 +13408,10 @@ packages:
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
- dev: true
/signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
- dev: true
/simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
@@ -13915,7 +13489,6 @@ packages:
/source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'}
- dev: true
/source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
@@ -13940,29 +13513,24 @@ packages:
/space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
- dev: true
/spdx-correct@3.2.0:
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
dependencies:
spdx-expression-parse: 3.0.1
spdx-license-ids: 3.0.18
- dev: true
/spdx-exceptions@2.5.0:
resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
- dev: true
/spdx-expression-parse@3.0.1:
resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
dependencies:
spdx-exceptions: 2.5.0
spdx-license-ids: 3.0.18
- dev: true
/spdx-license-ids@3.0.18:
resolution: {integrity: sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==}
- dev: true
/split2@1.1.1:
resolution: {integrity: sha512-cfurE2q8LamExY+lJ9Ex3ZfBwqAPduzOKVscPDXNCLLMvyaeD3DTz1yk7fVIs6Chco+12XeD0BB6HEoYzPYbXA==}
@@ -13990,7 +13558,6 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
minipass: 7.1.2
- dev: true
/stack-generator@2.0.10:
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
@@ -14037,7 +13604,6 @@ packages:
/stream-shift@1.0.3:
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
- dev: true
/stream-slice@0.1.2:
resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
@@ -14060,7 +13626,6 @@ packages:
/string-hash@1.1.3:
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
- dev: true
/string-width@2.1.1:
resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==}
@@ -14086,7 +13651,6 @@ packages:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
- dev: true
/string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
@@ -14095,7 +13659,6 @@ packages:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
- dev: true
/string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
@@ -14179,20 +13742,17 @@ packages:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
safe-buffer: 5.1.2
- dev: true
/string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies:
safe-buffer: 5.2.1
- dev: true
/stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
dependencies:
character-entities-html4: 2.1.0
character-entities-legacy: 3.0.0
- dev: true
/strip-ansi-control-characters@2.0.0:
resolution: {integrity: sha512-Q0/k5orrVGeaOlIOUn1gybGU0IcAbgHQT1faLo5hik4DqClKVSaka5xOhNNoRgtfztHVxCYxi7j71mrWom0bIw==}
@@ -14217,19 +13777,16 @@ packages:
engines: {node: '>=8'}
dependencies:
ansi-regex: 5.0.1
- dev: true
/strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
dependencies:
ansi-regex: 6.0.1
- dev: true
/strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
- dev: true
/strip-dirs@3.0.0:
resolution: {integrity: sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==}
@@ -14241,7 +13798,6 @@ packages:
/strip-final-newline@2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
- dev: true
/strip-final-newline@3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
@@ -14288,7 +13844,6 @@ packages:
resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==}
dependencies:
inline-style-parser: 0.1.1
- dev: true
/sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
@@ -14309,19 +13864,16 @@ packages:
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
- dev: true
/supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
dependencies:
has-flag: 4.0.0
- dev: true
/supports-color@9.4.0:
resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==}
engines: {node: '>=12'}
- dev: true
/supports-hyperlinks@2.3.0:
resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
@@ -14390,7 +13942,6 @@ packages:
mkdirp-classic: 0.5.3
pump: 3.0.0
tar-stream: 2.2.0
- dev: true
/tar-fs@3.0.6:
resolution: {integrity: sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==}
@@ -14411,7 +13962,6 @@ packages:
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.2
- dev: true
/tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
@@ -14431,7 +13981,6 @@ packages:
minizlib: 2.1.2
mkdirp: 1.0.4
yallist: 4.0.0
- dev: true
/temp-dir@3.0.0:
resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==}
@@ -14519,7 +14068,6 @@ packages:
dependencies:
readable-stream: 2.3.8
xtend: 4.0.2
- dev: true
/through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
@@ -14571,7 +14119,6 @@ packages:
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
- dev: true
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
@@ -14598,7 +14145,6 @@ packages:
/toml@3.0.0:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
- dev: true
/tomlify-j0.4@3.0.0:
resolution: {integrity: sha512-2Ulkc8T7mXJ2l0W476YC/A209PR38Nw8PuaCNtk9uI3t1zzFdGQeWYGQvmj2PZkVvRC/Yoi4xQKMRnWc/N29tQ==}
@@ -14621,7 +14167,6 @@ packages:
/trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
- dev: true
/trim-newlines@3.0.1:
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
@@ -14642,7 +14187,6 @@ packages:
/trough@2.2.0:
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
- dev: true
/ts-api-utils@1.3.0(typescript@5.4.5):
resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==}
@@ -14686,7 +14230,6 @@ packages:
typescript: 5.4.5
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
- dev: true
/tsconfck@3.0.3(typescript@5.4.5):
resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==}
@@ -14717,7 +14260,6 @@ packages:
json5: 2.2.3
minimist: 1.2.8
strip-bom: 3.0.0
- dev: true
/tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
@@ -14905,7 +14447,6 @@ packages:
/ufo@1.5.4:
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
- dev: true
/uid-safe@2.1.5:
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
@@ -14941,7 +14482,6 @@ packages:
/undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
- dev: true
/undici@6.19.7:
resolution: {integrity: sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==}
@@ -14972,21 +14512,18 @@ packages:
is-plain-obj: 4.1.0
trough: 2.2.0
vfile: 5.3.7
- dev: true
/unique-filename@3.0.0:
resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
unique-slug: 4.0.0
- dev: true
/unique-slug@4.0.0:
resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
imurmurhash: 0.1.4
- dev: true
/unique-string@3.0.0:
resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==}
@@ -14997,32 +14534,27 @@ packages:
/unist-util-generated@2.0.1:
resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==}
- dev: true
/unist-util-is@5.2.1:
resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==}
dependencies:
'@types/unist': 2.0.10
- dev: true
/unist-util-position-from-estree@1.1.2:
resolution: {integrity: sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==}
dependencies:
'@types/unist': 2.0.10
- dev: true
/unist-util-position@4.0.4:
resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==}
dependencies:
'@types/unist': 2.0.10
- dev: true
/unist-util-remove-position@4.0.2:
resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==}
dependencies:
'@types/unist': 2.0.10
unist-util-visit: 4.1.2
- dev: true
/unist-util-stringify-position@2.0.3:
resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==}
@@ -15034,14 +14566,12 @@ packages:
resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==}
dependencies:
'@types/unist': 2.0.10
- dev: true
/unist-util-visit-parents@5.1.3:
resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==}
dependencies:
'@types/unist': 2.0.10
unist-util-is: 5.2.1
- dev: true
/unist-util-visit@4.1.2:
resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==}
@@ -15049,7 +14579,6 @@ packages:
'@types/unist': 2.0.10
unist-util-is: 5.2.1
unist-util-visit-parents: 5.1.3
- dev: true
/universal-user-agent@6.0.1:
resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==}
@@ -15063,7 +14592,6 @@ packages:
/universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
- dev: true
/unix-dgram@2.0.6:
resolution: {integrity: sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg==}
@@ -15168,7 +14696,6 @@ packages:
browserslist: 4.23.3
escalade: 3.2.0
picocolors: 1.0.1
- dev: true
/update-notifier@7.0.0:
resolution: {integrity: sha512-Hv25Bh+eAbOLlsjJreVPOs4vd51rrtCrmhyOJtbpAojro34jS4KQaEp4/EvlHJX7jSO42VvEFpkastVyXyIsdQ==}
@@ -15203,7 +14730,6 @@ packages:
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- dev: true
/util@0.10.4:
resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==}
@@ -15244,11 +14770,9 @@ packages:
diff: 5.2.0
kleur: 4.1.5
sade: 1.8.1
- dev: true
/v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
- dev: true
/v8-compile-cache@2.4.0:
resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==}
@@ -15259,7 +14783,6 @@ packages:
dependencies:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
- dev: true
/validate-npm-package-name@4.0.0:
resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==}
@@ -15271,7 +14794,6 @@ packages:
/validate-npm-package-name@5.0.1:
resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- dev: true
/vandium-utils@1.2.0:
resolution: {integrity: sha512-yxYUDZz4BNo0CW/z5w4mvclitt5zolY7zjW97i6tTE+sU63cxYs1A6Bl9+jtIQa3+0hkeqY87k+7ptRvmeHe3g==}
@@ -15291,7 +14813,6 @@ packages:
dependencies:
'@types/unist': 2.0.10
unist-util-stringify-position: 3.0.3
- dev: true
/vfile@5.3.7:
resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==}
@@ -15300,7 +14821,6 @@ packages:
is-buffer: 2.0.5
unist-util-stringify-position: 3.0.3
vfile-message: 3.1.4
- dev: true
/vite-node@1.6.0(@types/node@20.16.3):
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
@@ -15321,7 +14841,6 @@ packages:
- sugarss
- supports-color
- terser
- dev: true
/vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.3.5):
resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==}
@@ -15374,7 +14893,6 @@ packages:
rollup: 4.20.0
optionalDependencies:
fsevents: 2.3.3
- dev: true
/vite@5.4.0(@types/node@20.16.3):
resolution: {integrity: sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==}
@@ -15413,7 +14931,6 @@ packages:
rollup: 4.20.0
optionalDependencies:
fsevents: 2.3.3
- dev: true
/vitest@1.6.0(@types/node@20.16.3):
resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
@@ -15487,7 +15004,6 @@ packages:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
dependencies:
defaults: 1.0.4
- dev: true
/web-encoding@1.1.5:
resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
@@ -15596,7 +15112,6 @@ packages:
hasBin: true
dependencies:
isexe: 2.0.0
- dev: true
/why-is-node-running@2.2.2:
resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
@@ -15665,7 +15180,6 @@ packages:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
- dev: true
/wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
@@ -15674,7 +15188,6 @@ packages:
ansi-styles: 6.2.1
string-width: 5.1.2
strip-ansi: 7.1.0
- dev: true
/wrap-ansi@9.0.0:
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
@@ -15687,7 +15200,6 @@ packages:
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
- dev: true
/write-file-atomic@3.0.3:
resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==}
@@ -15732,7 +15244,6 @@ packages:
optional: true
utf-8-validate:
optional: true
- dev: true
/ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
@@ -15764,7 +15275,6 @@ packages:
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
- dev: true
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
@@ -15773,11 +15283,9 @@ packages:
/yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
- dev: true
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
- dev: true
/yaml@2.4.5:
resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==}
@@ -15789,7 +15297,6 @@ packages:
resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==}
engines: {node: '>= 14'}
hasBin: true
- dev: true
/yargs-parser@20.2.9:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
@@ -15824,12 +15331,10 @@ packages:
/yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
- dev: true
/yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
- dev: true
/yocto-queue@1.0.0:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
@@ -15856,4 +15361,3 @@ packages:
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
- dev: true
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/.graphqlrc.js b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/.graphqlrc.js
new file mode 100644
index 000000000..2ed3a7634
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/.graphqlrc.js
@@ -0,0 +1,24 @@
+import { getSchema } from '@shopify/hydrogen-codegen'
+
+/**
+ * GraphQL Config
+ * @see https://the-guild.dev/graphql/config/docs/user/usage
+ * @type {IGraphQLConfig}
+ */
+export default {
+ projects: {
+ default: {
+ schema: getSchema('storefront'),
+ documents: ['./*.{ts,tsx,js,jsx}', './app/**/*.{ts,tsx,js,jsx}', '!./app/graphql/**/*.{ts,tsx,js,jsx}'],
+ },
+
+ customer: {
+ schema: getSchema('customer-account'),
+ documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'],
+ },
+
+ // Add your own GraphQL projects here for CMS, Shopify Admin API, etc.
+ },
+}
+
+/** @typedef {import('graphql-config').IGraphQLConfig} IGraphQLConfig */
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/README.md b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/README.md
new file mode 100644
index 000000000..edbf6867f
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/README.md
@@ -0,0 +1,52 @@
+# Hydrogen template: Skeleton
+
+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.
+
+[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/netlify/hydrogen-template#SESSION_SECRET=mock%20token&PUBLIC_STORE_DOMAIN=mock.shop)
+
+- [Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
+- [Get familiar with Remix](https://remix.run/docs/)
+
+## What's included
+
+- Remix 2
+- Hydrogen
+- Shopify CLI
+- ESLint
+- Prettier
+- GraphQL generator
+- TypeScript and JavaScript flavors
+- Minimal setup of components and routes
+
+## Getting started
+
+**Requirements:**
+
+- Node.js version 18.0.0 or higher
+- Netlify CLI 17.0.0 or higher
+
+```bash
+npm install -g netlify-cli@latest
+```
+
+[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/netlify/hydrogen-template#SESSION_SECRET=mock%20token&PUBLIC_STORE_DOMAIN=mock.shop)
+
+To create a new project, either click the "Deploy to Netlify" button above, or run the following command:
+
+```bash
+npx create-remix@latest --template=netlify/hydrogen-template
+```
+
+## Local development
+
+```bash
+npm run dev
+```
+
+## Building for production
+
+```bash
+npm run build
+```
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/assets/favicon.svg b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/assets/favicon.svg
new file mode 100644
index 000000000..f6c649733
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/assets/favicon.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/AddToCartButton.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/AddToCartButton.tsx
new file mode 100644
index 000000000..5a941eb71
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/AddToCartButton.tsx
@@ -0,0 +1,29 @@
+import { type FetcherWithComponents } from '@remix-run/react'
+import { CartForm, type OptimisticCartLineInput } from '@shopify/hydrogen'
+
+export function AddToCartButton({
+ analytics,
+ children,
+ disabled,
+ lines,
+ onClick,
+}: {
+ analytics?: unknown
+ children: React.ReactNode
+ disabled?: boolean
+ lines: Array
+ onClick?: () => void
+}) {
+ return (
+
+ {(fetcher: FetcherWithComponents) => (
+ <>
+
+
+ {children}
+
+ >
+ )}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Aside.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Aside.tsx
new file mode 100644
index 000000000..2e9f1ed2d
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Aside.tsx
@@ -0,0 +1,72 @@
+import { createContext, type ReactNode, useContext, useState } from 'react'
+
+type AsideType = 'search' | 'cart' | 'mobile' | 'closed'
+type AsideContextValue = {
+ type: AsideType
+ open: (mode: AsideType) => void
+ close: () => void
+}
+
+/**
+ * A side bar component with Overlay
+ * @example
+ * ```jsx
+ *
+ * ```
+ */
+export function Aside({
+ children,
+ heading,
+ type,
+}: {
+ children?: React.ReactNode
+ type: AsideType
+ heading: React.ReactNode
+}) {
+ const { type: activeType, close } = useAside()
+ const expanded = type === activeType
+
+ return (
+
+ )
+}
+
+const AsideContext = createContext(null)
+
+Aside.Provider = function AsideProvider({ children }: { children: ReactNode }) {
+ const [type, setType] = useState('closed')
+
+ return (
+ setType('closed'),
+ }}
+ >
+ {children}
+
+ )
+}
+
+export function useAside() {
+ const aside = useContext(AsideContext)
+ if (!aside) {
+ throw new Error('useAside must be used within an AsideProvider')
+ }
+ return aside
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartLineItem.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartLineItem.tsx
new file mode 100644
index 000000000..17e523d31
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartLineItem.tsx
@@ -0,0 +1,113 @@
+import type { CartLineUpdateInput } from '@shopify/hydrogen/storefront-api-types'
+import type { CartLayout } from '~/components/CartMain'
+import { CartForm, Image, type OptimisticCartLine } from '@shopify/hydrogen'
+import { useVariantUrl } from '~/lib/variants'
+import { Link } from '@remix-run/react'
+import { ProductPrice } from './ProductPrice'
+import { useAside } from './Aside'
+import type { CartApiQueryFragment } from 'storefrontapi.generated'
+
+type CartLine = OptimisticCartLine
+
+/**
+ * A single line item in the cart. It displays the product image, title, price.
+ * It also provides controls to update the quantity or remove the line item.
+ */
+export function CartLineItem({ layout, line }: { layout: CartLayout; line: CartLine }) {
+ const { id, merchandise } = line
+ const { product, title, image, selectedOptions } = merchandise
+ const lineItemUrl = useVariantUrl(product.handle, selectedOptions)
+ const { close } = useAside()
+
+ return (
+
+ {image && }
+
+
+
{
+ if (layout === 'aside') {
+ close()
+ }
+ }}
+ >
+
+ {product.title}
+
+
+
+
+ {selectedOptions.map((option) => (
+
+
+ {option.name}: {option.value}
+
+
+ ))}
+
+
+
+
+ )
+}
+
+/**
+ * Provides the controls to update the quantity of a line item in the cart.
+ * These controls are disabled when the line item is new, and the server
+ * hasn't yet responded that it was successfully added to the cart.
+ */
+function CartLineQuantity({ line }: { line: CartLine }) {
+ if (!line || typeof line?.quantity === 'undefined') return null
+ const { id: lineId, quantity, isOptimistic } = line
+ const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0))
+ const nextQuantity = Number((quantity + 1).toFixed(0))
+
+ return (
+
+ Quantity: {quantity}
+
+
+ −
+
+
+
+
+
+ +
+
+
+
+
+
+ )
+}
+
+/**
+ * A button that removes a line item from the cart. It is disabled
+ * when the line item is new, and the server hasn't yet responded
+ * that it was successfully added to the cart.
+ */
+function CartLineRemoveButton({ lineIds, disabled }: { lineIds: string[]; disabled: boolean }) {
+ return (
+
+
+ Remove
+
+
+ )
+}
+
+function CartLineUpdateButton({ children, lines }: { children: React.ReactNode; lines: CartLineUpdateInput[] }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartMain.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartMain.tsx
new file mode 100644
index 000000000..e076f93cf
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartMain.tsx
@@ -0,0 +1,58 @@
+import { useOptimisticCart } from '@shopify/hydrogen'
+import { Link } from '@remix-run/react'
+import type { CartApiQueryFragment } from 'storefrontapi.generated'
+import { useAside } from '~/components/Aside'
+import { CartLineItem } from '~/components/CartLineItem'
+import { CartSummary } from './CartSummary'
+
+export type CartLayout = 'page' | 'aside'
+
+export type CartMainProps = {
+ cart: CartApiQueryFragment | null
+ layout: CartLayout
+}
+
+/**
+ * The main cart component that displays the cart items and summary.
+ * It is used by both the /cart route and the cart aside dialog.
+ */
+export function CartMain({ layout, cart: originalCart }: CartMainProps) {
+ // The useOptimisticCart hook applies pending actions to the cart
+ // so the user immediately sees feedback when they modify the cart.
+ const cart = useOptimisticCart(originalCart)
+
+ const linesCount = Boolean(cart?.lines?.nodes?.length || 0)
+ const withDiscount = cart && Boolean(cart?.discountCodes?.filter((code) => code.applicable)?.length)
+ const className = `cart-main ${withDiscount ? 'with-discount' : ''}`
+ const cartHasItems = (cart?.totalQuantity ?? 0) > 0
+
+ return (
+
+
+
+
+
+ {(cart?.lines?.nodes ?? []).map((line) => (
+
+ ))}
+
+
+ {cartHasItems &&
}
+
+
+ )
+}
+
+function CartEmpty({ hidden = false }: { hidden: boolean; layout?: CartMainProps['layout'] }) {
+ const { close } = useAside()
+ return (
+
+
+
Looks like you haven’t added anything yet, let’s get you started!
+
+
+ Continue shopping →
+
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartSummary.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartSummary.tsx
new file mode 100644
index 000000000..2776c0a3f
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/CartSummary.tsx
@@ -0,0 +1,81 @@
+import type { CartApiQueryFragment } from 'storefrontapi.generated'
+import type { CartLayout } from '~/components/CartMain'
+import { CartForm, Money, type OptimisticCart } from '@shopify/hydrogen'
+
+type CartSummaryProps = {
+ cart: OptimisticCart
+ layout: CartLayout
+}
+
+export function CartSummary({ cart, layout }: CartSummaryProps) {
+ const className = layout === 'page' ? 'cart-summary-page' : 'cart-summary-aside'
+
+ return (
+
+
Totals
+
+ Subtotal
+ {cart.cost?.subtotalAmount?.amount ? : '-'}
+
+
+
+
+ )
+}
+function CartCheckoutActions({ checkoutUrl }: { checkoutUrl?: string }) {
+ if (!checkoutUrl) return null
+
+ return (
+
+ )
+}
+
+function CartDiscounts({ discountCodes }: { discountCodes?: CartApiQueryFragment['discountCodes'] }) {
+ const codes: string[] = discountCodes?.filter((discount) => discount.applicable)?.map(({ code }) => code) || []
+
+ return (
+
+ {/* Have existing discount, display it with a remove option */}
+
+
+
Discount(s)
+
+
+ {codes?.join(', ')}
+
+ Remove
+
+
+
+
+
+ {/* Show an input to apply a discount */}
+
+
+
+
+ Apply
+
+
+
+ )
+}
+
+function UpdateDiscountForm({ discountCodes, children }: { discountCodes?: string[]; children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Footer.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Footer.tsx
new file mode 100644
index 000000000..3939c83cc
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Footer.tsx
@@ -0,0 +1,113 @@
+import { Suspense } from 'react'
+import { Await, NavLink } from '@remix-run/react'
+import type { FooterQuery, HeaderQuery } from 'storefrontapi.generated'
+
+interface FooterProps {
+ footer: Promise
+ header: HeaderQuery
+ publicStoreDomain: string
+}
+
+export function Footer({ footer: footerPromise, header, publicStoreDomain }: FooterProps) {
+ return (
+
+
+ {(footer) => (
+
+ {footer?.menu && header.shop.primaryDomain?.url && (
+
+ )}
+
+ )}
+
+
+ )
+}
+
+function FooterMenu({
+ menu,
+ primaryDomainUrl,
+ publicStoreDomain,
+}: {
+ menu: FooterQuery['menu']
+ primaryDomainUrl: FooterProps['header']['shop']['primaryDomain']['url']
+ publicStoreDomain: string
+}) {
+ return (
+
+ {(menu || FALLBACK_FOOTER_MENU).items.map((item) => {
+ if (!item.url) return null
+ // if the url is internal, we strip the domain
+ const url =
+ item.url.includes('myshopify.com') ||
+ item.url.includes(publicStoreDomain) ||
+ item.url.includes(primaryDomainUrl)
+ ? new URL(item.url).pathname
+ : item.url
+ const isExternal = !url.startsWith('/')
+ return isExternal ? (
+
+ {item.title}
+
+ ) : (
+
+ {item.title}
+
+ )
+ })}
+
+ )
+}
+
+const FALLBACK_FOOTER_MENU = {
+ id: 'gid://shopify/Menu/199655620664',
+ items: [
+ {
+ id: 'gid://shopify/MenuItem/461633060920',
+ resourceId: 'gid://shopify/ShopPolicy/23358046264',
+ tags: [],
+ title: 'Privacy Policy',
+ type: 'SHOP_POLICY',
+ url: '/policies/privacy-policy',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461633093688',
+ resourceId: 'gid://shopify/ShopPolicy/23358013496',
+ tags: [],
+ title: 'Refund Policy',
+ type: 'SHOP_POLICY',
+ url: '/policies/refund-policy',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461633126456',
+ resourceId: 'gid://shopify/ShopPolicy/23358111800',
+ tags: [],
+ title: 'Shipping Policy',
+ type: 'SHOP_POLICY',
+ url: '/policies/shipping-policy',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461633159224',
+ resourceId: 'gid://shopify/ShopPolicy/23358079032',
+ tags: [],
+ title: 'Terms of Service',
+ type: 'SHOP_POLICY',
+ url: '/policies/terms-of-service',
+ items: [],
+ },
+ ],
+}
+
+function activeLinkStyle({ isActive, isPending }: { isActive: boolean; isPending: boolean }) {
+ return {
+ fontWeight: isActive ? 'bold' : undefined,
+ color: isPending ? 'grey' : 'white',
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Header.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Header.tsx
new file mode 100644
index 000000000..5d714c27f
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/Header.tsx
@@ -0,0 +1,193 @@
+import { Suspense } from 'react'
+import { Await, NavLink } from '@remix-run/react'
+import { type CartViewPayload, useAnalytics } from '@shopify/hydrogen'
+import type { HeaderQuery, CartApiQueryFragment } from 'storefrontapi.generated'
+import { useAside } from '~/components/Aside'
+
+interface HeaderProps {
+ header: HeaderQuery
+ cart: Promise
+ publicStoreDomain: string
+}
+
+type Viewport = 'desktop' | 'mobile'
+
+export function Header({ header, cart, publicStoreDomain }: HeaderProps) {
+ const { shop, menu } = header
+ return (
+
+ )
+}
+
+export function HeaderMenu({
+ menu,
+ primaryDomainUrl,
+ viewport,
+ publicStoreDomain,
+}: {
+ menu: HeaderProps['header']['menu']
+ primaryDomainUrl: HeaderProps['header']['shop']['primaryDomain']['url']
+ viewport: Viewport
+ publicStoreDomain: HeaderProps['publicStoreDomain']
+}) {
+ const className = `header-menu-${viewport}`
+ const { close } = useAside()
+
+ return (
+
+ {viewport === 'mobile' && (
+
+ Home
+
+ )}
+ {(menu || FALLBACK_HEADER_MENU).items.map((item) => {
+ if (!item.url) return null
+
+ // if the url is internal, we strip the domain
+ const url =
+ item.url.includes('myshopify.com') ||
+ item.url.includes(publicStoreDomain) ||
+ item.url.includes(primaryDomainUrl)
+ ? new URL(item.url).pathname
+ : item.url
+ return (
+
+ {item.title}
+
+ )
+ })}
+
+ )
+}
+
+function HeaderCtas({ cart }: Pick) {
+ return (
+
+
+
+
+
+ )
+}
+
+function HeaderMenuMobileToggle() {
+ const { open } = useAside()
+ return (
+ open('mobile')}>
+ ☰
+
+ )
+}
+
+function SearchToggle() {
+ const { open } = useAside()
+ return (
+ open('search')}>
+ Search
+
+ )
+}
+
+function CartBadge({ count }: { count: number | null }) {
+ const { open } = useAside()
+ const { publish, shop, cart, prevCart } = useAnalytics()
+
+ return (
+ {
+ e.preventDefault()
+ open('cart')
+ publish('cart_viewed', {
+ cart,
+ prevCart,
+ shop,
+ url: window.location.href || '',
+ } as CartViewPayload)
+ }}
+ >
+ Cart {count === null ? : count}
+
+ )
+}
+
+function CartToggle({ cart }: Pick) {
+ return (
+ }>
+
+ {(cart) => {
+ if (!cart) return
+ return
+ }}
+
+
+ )
+}
+
+const FALLBACK_HEADER_MENU = {
+ id: 'gid://shopify/Menu/199655587896',
+ items: [
+ {
+ id: 'gid://shopify/MenuItem/461609500728',
+ resourceId: null,
+ tags: [],
+ title: 'Collections',
+ type: 'HTTP',
+ url: '/collections',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461609533496',
+ resourceId: null,
+ tags: [],
+ title: 'Blog',
+ type: 'HTTP',
+ url: '/blogs/journal',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461609566264',
+ resourceId: null,
+ tags: [],
+ title: 'Policies',
+ type: 'HTTP',
+ url: '/policies',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461609599032',
+ resourceId: 'gid://shopify/Page/92591030328',
+ tags: [],
+ title: 'About',
+ type: 'PAGE',
+ url: '/pages/about',
+ items: [],
+ },
+ ],
+}
+
+function activeLinkStyle({ isActive, isPending }: { isActive: boolean; isPending: boolean }) {
+ return {
+ fontWeight: isActive ? 'bold' : undefined,
+ color: isPending ? 'grey' : 'black',
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/PageLayout.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/PageLayout.tsx
new file mode 100644
index 000000000..98dd86d1e
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/PageLayout.tsx
@@ -0,0 +1,124 @@
+import { Await, Link } from '@remix-run/react'
+import { Suspense } from 'react'
+import type { CartApiQueryFragment, FooterQuery, HeaderQuery } from 'storefrontapi.generated'
+import { Aside } from '~/components/Aside'
+import { Footer } from '~/components/Footer'
+import { Header, HeaderMenu } from '~/components/Header'
+import { CartMain } from '~/components/CartMain'
+import { SEARCH_ENDPOINT, SearchFormPredictive } from '~/components/SearchFormPredictive'
+import { SearchResultsPredictive } from '~/components/SearchResultsPredictive'
+
+interface PageLayoutProps {
+ cart: Promise
+ footer: Promise
+ header: HeaderQuery
+ publicStoreDomain: string
+ children?: React.ReactNode
+}
+
+export function PageLayout({ cart, children = null, footer, header, publicStoreDomain }: PageLayoutProps) {
+ return (
+
+
+
+
+ {header && }
+ {children}
+
+
+ )
+}
+
+function CartAside({ cart }: { cart: PageLayoutProps['cart'] }) {
+ return (
+
}>
+
+ {(cart) => {
+ return
+ }}
+
+
+
+ )
+}
+
+function SearchAside() {
+ return (
+
+
+
+
+ {({ fetchResults, goToSearch, inputRef }) => (
+ <>
+
+
+ Search
+ >
+ )}
+
+
+
+ {({ items, total, term, state, inputRef, closeSearch }) => {
+ const { articles, collections, pages, products, queries } = items
+
+ if (state === 'loading' && term.current) {
+ return Loading...
+ }
+
+ if (!total) {
+ return
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {term.current && total ? (
+
+
+ View all results for {term.current}
+ →
+
+
+ ) : null}
+ >
+ )
+ }}
+
+
+
+ )
+}
+
+function MobileMenuAside({
+ header,
+ publicStoreDomain,
+}: {
+ header: PageLayoutProps['header']
+ publicStoreDomain: PageLayoutProps['publicStoreDomain']
+}) {
+ return (
+ header.menu &&
+ header.shop.primaryDomain?.url && (
+
+ )
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/PaginatedResourceSection.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/PaginatedResourceSection.tsx
new file mode 100644
index 000000000..90c3db885
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/PaginatedResourceSection.tsx
@@ -0,0 +1,32 @@
+import * as React from 'react'
+import { Pagination } from '@shopify/hydrogen'
+
+/**
+ * is a component that encapsulate how the previous and next behaviors throughout your application.
+ */
+
+export function PaginatedResourceSection({
+ connection,
+ children,
+ resourcesClassName,
+}: {
+ connection: React.ComponentProps>['connection']
+ children: React.FunctionComponent<{ node: NodesType; index: number }>
+ resourcesClassName?: string
+}) {
+ return (
+
+ {({ nodes, isLoading, PreviousLink, NextLink }) => {
+ const resoucesMarkup = nodes.map((node, index) => children({ node, index }))
+
+ return (
+
+
{isLoading ? 'Loading...' : ↑ Load previous }
+ {resourcesClassName ?
{resoucesMarkup}
: resoucesMarkup}
+
{isLoading ? 'Loading...' : Load more ↓ }
+
+ )
+ }}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductForm.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductForm.tsx
new file mode 100644
index 000000000..8b74f4db3
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductForm.tsx
@@ -0,0 +1,77 @@
+import { Link } from '@remix-run/react'
+import { type VariantOption, VariantSelector } from '@shopify/hydrogen'
+import type { ProductFragment, ProductVariantFragment } from 'storefrontapi.generated'
+import { AddToCartButton } from '~/components/AddToCartButton'
+import { useAside } from '~/components/Aside'
+
+export function ProductForm({
+ product,
+ selectedVariant,
+ variants,
+}: {
+ product: ProductFragment
+ selectedVariant: ProductFragment['selectedVariant']
+ variants: Array
+}) {
+ const { open } = useAside()
+ return (
+
+
option.values.length > 1)}
+ variants={variants}
+ >
+ {({ option }) => }
+
+
+
{
+ open('cart')
+ }}
+ lines={
+ selectedVariant
+ ? [
+ {
+ merchandiseId: selectedVariant.id,
+ quantity: 1,
+ selectedVariant,
+ },
+ ]
+ : []
+ }
+ >
+ {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
+
+
+ )
+}
+
+function ProductOptions({ option }: { option: VariantOption }) {
+ return (
+
+
{option.name}
+
+ {option.values.map(({ value, isAvailable, isActive, to }) => {
+ return (
+
+ {value}
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductImage.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductImage.tsx
new file mode 100644
index 000000000..4cd55b064
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductImage.tsx
@@ -0,0 +1,19 @@
+import type { ProductVariantFragment } from 'storefrontapi.generated'
+import { Image } from '@shopify/hydrogen'
+
+export function ProductImage({ image }: { image: ProductVariantFragment['image'] }) {
+ if (!image) {
+ return
+ }
+ return (
+
+
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductPrice.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductPrice.tsx
new file mode 100644
index 000000000..4bbd3c20a
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/ProductPrice.tsx
@@ -0,0 +1,21 @@
+import { Money } from '@shopify/hydrogen'
+import type { MoneyV2 } from '@shopify/hydrogen/storefront-api-types'
+
+export function ProductPrice({ price, compareAtPrice }: { price?: MoneyV2; compareAtPrice?: MoneyV2 | null }) {
+ return (
+
+ {compareAtPrice ? (
+
+ {price ? : null}
+
+
+
+
+ ) : price ? (
+
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchForm.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchForm.tsx
new file mode 100644
index 000000000..8f08ce33f
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchForm.tsx
@@ -0,0 +1,66 @@
+import { useRef, useEffect } from 'react'
+import { Form, type FormProps } from '@remix-run/react'
+
+type SearchFormProps = Omit & {
+ children: (args: { inputRef: React.RefObject }) => React.ReactNode
+}
+
+/**
+ * Search form component that sends search requests to the `/search` route.
+ * @example
+ * ```tsx
+ *
+ * {({inputRef}) => (
+ * <>
+ *
+ * Search
+ * >
+ * )}
+ *
+ */
+export function SearchForm({ children, ...props }: SearchFormProps) {
+ const inputRef = useRef(null)
+
+ useFocusOnCmdK(inputRef)
+
+ if (typeof children !== 'function') {
+ return null
+ }
+
+ return (
+
+ )
+}
+
+/**
+ * Focuses the input when cmd+k is pressed
+ */
+function useFocusOnCmdK(inputRef: React.RefObject) {
+ // focus the input when cmd+k is pressed
+ useEffect(() => {
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === 'k' && event.metaKey) {
+ event.preventDefault()
+ inputRef.current?.focus()
+ }
+
+ if (event.key === 'Escape') {
+ inputRef.current?.blur()
+ }
+ }
+
+ document.addEventListener('keydown', handleKeyDown)
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [inputRef])
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchFormPredictive.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchFormPredictive.tsx
new file mode 100644
index 000000000..a8831fd67
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchFormPredictive.tsx
@@ -0,0 +1,71 @@
+import { useFetcher, useNavigate, type FormProps, type Fetcher } from '@remix-run/react'
+import React, { useRef, useEffect } from 'react'
+import type { PredictiveSearchReturn } from '~/lib/search'
+import { useAside } from './Aside'
+
+type SearchFormPredictiveChildren = (args: {
+ fetchResults: (event: React.ChangeEvent) => void
+ goToSearch: () => void
+ inputRef: React.MutableRefObject
+ fetcher: Fetcher
+}) => React.ReactNode
+
+type SearchFormPredictiveProps = Omit & {
+ children: SearchFormPredictiveChildren | null
+}
+
+export const SEARCH_ENDPOINT = '/search'
+
+/**
+ * Search form component that sends search requests to the `/search` route
+ **/
+export function SearchFormPredictive({
+ children,
+ className = 'predictive-search-form',
+ ...props
+}: SearchFormPredictiveProps) {
+ const fetcher = useFetcher({ key: 'search' })
+ const inputRef = useRef(null)
+ const navigate = useNavigate()
+ const aside = useAside()
+
+ /** Reset the input value and blur the input */
+ function resetInput(event: React.FormEvent) {
+ event.preventDefault()
+ event.stopPropagation()
+ if (inputRef?.current?.value) {
+ inputRef.current.blur()
+ }
+ }
+
+ /** Navigate to the search page with the current input value */
+ function goToSearch() {
+ const term = inputRef?.current?.value
+ navigate(SEARCH_ENDPOINT + (term ? `?q=${term}` : ''))
+ aside.close()
+ }
+
+ /** Fetch search results based on the input value */
+ function fetchResults(event: React.ChangeEvent) {
+ fetcher.submit(
+ { q: event.target.value || '', limit: 5, predictive: true },
+ { method: 'GET', action: SEARCH_ENDPOINT },
+ )
+ }
+
+ // ensure the passed input has a type of search, because SearchResults
+ // will select the element based on the input
+ useEffect(() => {
+ inputRef?.current?.setAttribute('type', 'search')
+ }, [])
+
+ if (typeof children !== 'function') {
+ return null
+ }
+
+ return (
+
+ {children({ inputRef, fetcher, fetchResults, goToSearch })}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchResults.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchResults.tsx
new file mode 100644
index 000000000..d6714348c
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchResults.tsx
@@ -0,0 +1,143 @@
+import { Link } from '@remix-run/react'
+import { Image, Money, Pagination } from '@shopify/hydrogen'
+import { urlWithTrackingParams, type RegularSearchReturn } from '~/lib/search'
+
+type SearchItems = RegularSearchReturn['result']['items']
+type PartialSearchResult = Pick &
+ Pick
+
+type SearchResultsProps = RegularSearchReturn & {
+ children: (args: SearchItems & { term: string }) => React.ReactNode
+}
+
+export function SearchResults({ term, result, children }: Omit) {
+ if (!result?.total) {
+ return null
+ }
+
+ return children({ ...result.items, term })
+}
+
+SearchResults.Articles = SearchResultsArticles
+SearchResults.Pages = SearchResultsPages
+SearchResults.Products = SearchResultsProducts
+SearchResults.Empty = SearchResultsEmpty
+
+function SearchResultsArticles({ term, articles }: PartialSearchResult<'articles'>) {
+ if (!articles?.nodes.length) {
+ return null
+ }
+
+ return (
+
+
Articles
+
+ {articles?.nodes?.map((article) => {
+ const articleUrl = urlWithTrackingParams({
+ baseUrl: `/blogs/${article.handle}`,
+ trackingParams: article.trackingParameters,
+ term,
+ })
+
+ return (
+
+
+ {article.title}
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+function SearchResultsPages({ term, pages }: PartialSearchResult<'pages'>) {
+ if (!pages?.nodes.length) {
+ return null
+ }
+
+ return (
+
+
Pages
+
+ {pages?.nodes?.map((page) => {
+ const pageUrl = urlWithTrackingParams({
+ baseUrl: `/pages/${page.handle}`,
+ trackingParams: page.trackingParameters,
+ term,
+ })
+
+ return (
+
+
+ {page.title}
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+function SearchResultsProducts({ term, products }: PartialSearchResult<'products'>) {
+ if (!products?.nodes.length) {
+ return null
+ }
+
+ return (
+
+
Products
+
+ {({ nodes, isLoading, NextLink, PreviousLink }) => {
+ const ItemsMarkup = nodes.map((product) => {
+ const productUrl = urlWithTrackingParams({
+ baseUrl: `/products/${product.handle}`,
+ trackingParams: product.trackingParameters,
+ term,
+ })
+
+ return (
+
+
+ {product.variants.nodes[0].image && (
+
+ )}
+
+
{product.title}
+
+
+
+
+
+
+ )
+ })
+
+ return (
+
+
+
{isLoading ? 'Loading...' : ↑ Load previous }
+
+
+ {ItemsMarkup}
+
+
+
+ {isLoading ? 'Loading...' : Load more ↓ }
+
+
+ )
+ }}
+
+
+
+ )
+}
+
+function SearchResultsEmpty() {
+ return No results, try a different search.
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchResultsPredictive.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchResultsPredictive.tsx
new file mode 100644
index 000000000..bf3aa9bc8
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/components/SearchResultsPredictive.tsx
@@ -0,0 +1,273 @@
+import { Link, useFetcher, type Fetcher } from '@remix-run/react'
+import { Image, Money } from '@shopify/hydrogen'
+import React, { useRef, useEffect } from 'react'
+import { getEmptyPredictiveSearchResult, urlWithTrackingParams, type PredictiveSearchReturn } from '~/lib/search'
+import { useAside } from './Aside'
+
+type PredictiveSearchItems = PredictiveSearchReturn['result']['items']
+
+type UsePredictiveSearchReturn = {
+ term: React.MutableRefObject
+ total: number
+ inputRef: React.MutableRefObject
+ items: PredictiveSearchItems
+ fetcher: Fetcher
+}
+
+type SearchResultsPredictiveArgs = Pick & {
+ state: Fetcher['state']
+ closeSearch: () => void
+}
+
+type PartialPredictiveSearchResult<
+ ItemType extends keyof PredictiveSearchItems,
+ ExtraProps extends keyof SearchResultsPredictiveArgs = 'term' | 'closeSearch',
+> = Pick & Pick
+
+type SearchResultsPredictiveProps = {
+ children: (args: SearchResultsPredictiveArgs) => React.ReactNode
+}
+
+/**
+ * Component that renders predictive search results
+ */
+export function SearchResultsPredictive({ children }: SearchResultsPredictiveProps) {
+ const aside = useAside()
+ const { term, inputRef, fetcher, total, items } = usePredictiveSearch()
+
+ /*
+ * Utility that resets the search input
+ */
+ function resetInput() {
+ if (inputRef.current) {
+ inputRef.current.blur()
+ inputRef.current.value = ''
+ }
+ }
+
+ /**
+ * Utility that resets the search input and closes the search aside
+ */
+ function closeSearch() {
+ resetInput()
+ aside.close()
+ }
+
+ return children({
+ items,
+ closeSearch,
+ inputRef,
+ state: fetcher.state,
+ term,
+ total,
+ })
+}
+
+SearchResultsPredictive.Articles = SearchResultsPredictiveArticles
+SearchResultsPredictive.Collections = SearchResultsPredictiveCollections
+SearchResultsPredictive.Pages = SearchResultsPredictivePages
+SearchResultsPredictive.Products = SearchResultsPredictiveProducts
+SearchResultsPredictive.Queries = SearchResultsPredictiveQueries
+SearchResultsPredictive.Empty = SearchResultsPredictiveEmpty
+
+function SearchResultsPredictiveArticles({ term, articles, closeSearch }: PartialPredictiveSearchResult<'articles'>) {
+ if (!articles.length) return null
+
+ return (
+
+
Articles
+
+ {articles.map((article) => {
+ const articleUrl = urlWithTrackingParams({
+ baseUrl: `/blogs/${article.blog.handle}/${article.handle}`,
+ trackingParams: article.trackingParameters,
+ term: term.current ?? '',
+ })
+
+ return (
+
+
+ {article.image?.url && (
+
+ )}
+
+ {article.title}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function SearchResultsPredictiveCollections({
+ term,
+ collections,
+ closeSearch,
+}: PartialPredictiveSearchResult<'collections'>) {
+ if (!collections.length) return null
+
+ return (
+
+
Collections
+
+ {collections.map((collection) => {
+ const colllectionUrl = urlWithTrackingParams({
+ baseUrl: `/collections/${collection.handle}`,
+ trackingParams: collection.trackingParameters,
+ term: term.current,
+ })
+
+ return (
+
+
+ {collection.image?.url && (
+
+ )}
+
+ {collection.title}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function SearchResultsPredictivePages({ term, pages, closeSearch }: PartialPredictiveSearchResult<'pages'>) {
+ if (!pages.length) return null
+
+ return (
+
+
Pages
+
+ {pages.map((page) => {
+ const pageUrl = urlWithTrackingParams({
+ baseUrl: `/pages/${page.handle}`,
+ trackingParams: page.trackingParameters,
+ term: term.current,
+ })
+
+ return (
+
+
+
+ {page.title}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function SearchResultsPredictiveProducts({ term, products, closeSearch }: PartialPredictiveSearchResult<'products'>) {
+ if (!products.length) return null
+
+ return (
+
+
Products
+
+ {products.map((product) => {
+ const productUrl = urlWithTrackingParams({
+ baseUrl: `/products/${product.handle}`,
+ trackingParams: product.trackingParameters,
+ term: term.current,
+ })
+
+ const image = product?.variants?.nodes?.[0].image
+ return (
+
+
+ {image && }
+
+
{product.title}
+
+ {product?.variants?.nodes?.[0].price && }
+
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function SearchResultsPredictiveQueries({ queries, inputRef }: PartialPredictiveSearchResult<'queries', 'inputRef'>) {
+ if (!queries.length) return null
+
+ return (
+
+ )
+}
+
+const FEATURED_COLLECTION_QUERY = `#graphql
+ fragment FeaturedCollection on Collection {
+ id
+ title
+ image {
+ id
+ url
+ altText
+ width
+ height
+ }
+ handle
+ }
+ query FeaturedCollection($country: CountryCode, $language: LanguageCode)
+ @inContext(country: $country, language: $language) {
+ collections(first: 1, sortKey: UPDATED_AT, reverse: true) {
+ nodes {
+ ...FeaturedCollection
+ }
+ }
+ }
+` as const
+
+const RECOMMENDED_PRODUCTS_QUERY = `#graphql
+ fragment RecommendedProduct on Product {
+ id
+ title
+ handle
+ priceRange {
+ minVariantPrice {
+ amount
+ currencyCode
+ }
+ }
+ images(first: 1) {
+ nodes {
+ id
+ url
+ altText
+ width
+ height
+ }
+ }
+ }
+ query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
+ @inContext(country: $country, language: $language) {
+ products(first: 4, sortKey: UPDATED_AT, reverse: true) {
+ nodes {
+ ...RecommendedProduct
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs.$blogHandle.$articleHandle.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs.$blogHandle.$articleHandle.tsx
new file mode 100644
index 000000000..d1b100e09
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs.$blogHandle.$articleHandle.tsx
@@ -0,0 +1,110 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, type MetaFunction } from '@remix-run/react'
+import { Image } from '@shopify/hydrogen'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.article.title ?? ''} article` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, params }: LoaderFunctionArgs) {
+ const { blogHandle, articleHandle } = params
+
+ if (!articleHandle || !blogHandle) {
+ throw new Response('Not found', { status: 404 })
+ }
+
+ const [{ blog }] = await Promise.all([
+ context.storefront.query(ARTICLE_QUERY, {
+ variables: { blogHandle, articleHandle },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ if (!blog?.articleByHandle) {
+ throw new Response(null, { status: 404 })
+ }
+
+ const article = blog.articleByHandle
+
+ return { article }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Article() {
+ const { article } = useLoaderData()
+ const { title, image, contentHtml, author } = article
+
+ const publishedDate = new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(article.publishedAt))
+
+ return (
+
+
+ {title}
+
+ {publishedDate} · {author?.name}
+
+
+
+ {image &&
}
+
+
+ )
+}
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog#field-blog-articlebyhandle
+const ARTICLE_QUERY = `#graphql
+ query Article(
+ $articleHandle: String!
+ $blogHandle: String!
+ $country: CountryCode
+ $language: LanguageCode
+ ) @inContext(language: $language, country: $country) {
+ blog(handle: $blogHandle) {
+ articleByHandle(handle: $articleHandle) {
+ title
+ contentHtml
+ publishedAt
+ author: authorV2 {
+ name
+ }
+ image {
+ id
+ altText
+ url
+ width
+ height
+ }
+ seo {
+ description
+ title
+ }
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs.$blogHandle._index.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs.$blogHandle._index.tsx
new file mode 100644
index 000000000..f5b3c00b5
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs.$blogHandle._index.tsx
@@ -0,0 +1,161 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { Link, useLoaderData, type MetaFunction } from '@remix-run/react'
+import { Image, getPaginationVariables } from '@shopify/hydrogen'
+import type { ArticleItemFragment } from 'storefrontapi.generated'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.blog.title ?? ''} blog` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, request, params }: LoaderFunctionArgs) {
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 4,
+ })
+
+ if (!params.blogHandle) {
+ throw new Response(`blog not found`, { status: 404 })
+ }
+
+ const [{ blog }] = await Promise.all([
+ context.storefront.query(BLOGS_QUERY, {
+ variables: {
+ blogHandle: params.blogHandle,
+ ...paginationVariables,
+ },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ if (!blog?.articles) {
+ throw new Response('Not found', { status: 404 })
+ }
+
+ return { blog }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Blog() {
+ const { blog } = useLoaderData()
+ const { articles } = blog
+
+ return (
+
+
{blog.title}
+
+
+ {({ node: article, index }) => (
+
+ )}
+
+
+
+ )
+}
+
+function ArticleItem({ article, loading }: { article: ArticleItemFragment; loading?: HTMLImageElement['loading'] }) {
+ const publishedAt = new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(article.publishedAt!))
+ return (
+
+
+ {article.image && (
+
+
+
+ )}
+
{article.title}
+
{publishedAt}
+
+
+ )
+}
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog
+const BLOGS_QUERY = `#graphql
+ query Blog(
+ $language: LanguageCode
+ $blogHandle: String!
+ $first: Int
+ $last: Int
+ $startCursor: String
+ $endCursor: String
+ ) @inContext(language: $language) {
+ blog(handle: $blogHandle) {
+ title
+ seo {
+ title
+ description
+ }
+ articles(
+ first: $first,
+ last: $last,
+ before: $startCursor,
+ after: $endCursor
+ ) {
+ nodes {
+ ...ArticleItem
+ }
+ pageInfo {
+ hasPreviousPage
+ hasNextPage
+ hasNextPage
+ endCursor
+ startCursor
+ }
+
+ }
+ }
+ }
+ fragment ArticleItem on Article {
+ author: authorV2 {
+ name
+ }
+ contentHtml
+ handle
+ id
+ image {
+ id
+ altText
+ url
+ width
+ height
+ }
+ publishedAt
+ title
+ blog {
+ handle
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs._index.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs._index.tsx
new file mode 100644
index 000000000..87b8388a1
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/blogs._index.tsx
@@ -0,0 +1,101 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { Link, useLoaderData, type MetaFunction } from '@remix-run/react'
+import { getPaginationVariables } from '@shopify/hydrogen'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export const meta: MetaFunction = () => {
+ return [{ title: `Hydrogen | Blogs` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, request }: LoaderFunctionArgs) {
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 10,
+ })
+
+ const [{ blogs }] = await Promise.all([
+ context.storefront.query(BLOGS_QUERY, {
+ variables: {
+ ...paginationVariables,
+ },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ return { blogs }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Blogs() {
+ const { blogs } = useLoaderData()
+
+ return (
+
+
Blogs
+
+
+ {({ node: blog }) => (
+
+ {blog.title}
+
+ )}
+
+
+
+ )
+}
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog
+const BLOGS_QUERY = `#graphql
+ query Blogs(
+ $country: CountryCode
+ $endCursor: String
+ $first: Int
+ $language: LanguageCode
+ $last: Int
+ $startCursor: String
+ ) @inContext(country: $country, language: $language) {
+ blogs(
+ first: $first,
+ last: $last,
+ before: $startCursor,
+ after: $endCursor
+ ) {
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
+ nodes {
+ title
+ handle
+ seo {
+ title
+ description
+ }
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/cart.$lines.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/cart.$lines.tsx
new file mode 100644
index 000000000..45e267950
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/cart.$lines.tsx
@@ -0,0 +1,69 @@
+import { redirect, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+
+/**
+ * Automatically creates a new cart based on the URL and redirects straight to checkout.
+ * Expected URL structure:
+ * ```js
+ * /cart/:
+ *
+ * ```
+ *
+ * More than one `:` separated by a comma, can be supplied in the URL, for
+ * carts with more than one product variant.
+ *
+ * @example
+ * Example path creating a cart with two product variants, different quantities, and a discount code in the querystring:
+ * ```js
+ * /cart/41007289663544:1,41007289696312:2?discount=HYDROBOARD
+ *
+ * ```
+ */
+export async function loader({ request, context, params }: LoaderFunctionArgs) {
+ const { cart } = context
+ const { lines } = params
+ if (!lines) return redirect('/cart')
+ const linesMap = lines.split(',').map((line) => {
+ const lineDetails = line.split(':')
+ const variantId = lineDetails[0]
+ const quantity = parseInt(lineDetails[1], 10)
+
+ return {
+ merchandiseId: `gid://shopify/ProductVariant/${variantId}`,
+ quantity,
+ }
+ })
+
+ const url = new URL(request.url)
+ const searchParams = new URLSearchParams(url.search)
+
+ const discount = searchParams.get('discount')
+ const discountArray = discount ? [discount] : []
+
+ // create a cart
+ const result = await cart.create({
+ lines: linesMap,
+ discountCodes: discountArray,
+ })
+
+ const cartResult = result.cart
+
+ if (result.errors?.length || !cartResult) {
+ throw new Response('Link may be expired. Try checking the URL.', {
+ status: 410,
+ })
+ }
+
+ // Update cart id in cookie
+ const headers = cart.setCartId(cartResult.id)
+
+ // redirect to checkout
+ if (cartResult.checkoutUrl) {
+ return redirect(cartResult.checkoutUrl, { headers })
+ } else {
+ throw new Error('No checkout URL found')
+ }
+}
+
+export default function Component() {
+ return null
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/cart.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/cart.tsx
new file mode 100644
index 000000000..16f70173c
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/cart.tsx
@@ -0,0 +1,97 @@
+import { Await, type MetaFunction, useRouteLoaderData } from '@remix-run/react'
+import { Suspense } from 'react'
+import type { CartQueryDataReturn } from '@shopify/hydrogen'
+import { CartForm } from '@shopify/hydrogen'
+import { json, type ActionFunctionArgs } from '@netlify/remix-runtime'
+import { CartMain } from '~/components/CartMain'
+import type { RootLoader } from '~/root'
+
+export const meta: MetaFunction = () => {
+ return [{ title: `Hydrogen | Cart` }]
+}
+
+export async function action({ request, context }: ActionFunctionArgs) {
+ const { cart } = context
+
+ const formData = await request.formData()
+
+ const { action, inputs } = CartForm.getFormInput(formData)
+
+ if (!action) {
+ throw new Error('No action provided')
+ }
+
+ let status = 200
+ let result: CartQueryDataReturn
+
+ switch (action) {
+ case CartForm.ACTIONS.LinesAdd:
+ result = await cart.addLines(inputs.lines)
+ break
+ case CartForm.ACTIONS.LinesUpdate:
+ result = await cart.updateLines(inputs.lines)
+ break
+ case CartForm.ACTIONS.LinesRemove:
+ result = await cart.removeLines(inputs.lineIds)
+ break
+ case CartForm.ACTIONS.DiscountCodesUpdate: {
+ const formDiscountCode = inputs.discountCode
+
+ // User inputted discount code
+ const discountCodes = (formDiscountCode ? [formDiscountCode] : []) as string[]
+
+ // Combine discount codes already applied on cart
+ discountCodes.push(...inputs.discountCodes)
+
+ result = await cart.updateDiscountCodes(discountCodes)
+ break
+ }
+ case CartForm.ACTIONS.BuyerIdentityUpdate: {
+ result = await cart.updateBuyerIdentity({
+ ...inputs.buyerIdentity,
+ })
+ break
+ }
+ default:
+ throw new Error(`${action} cart action is not defined`)
+ }
+
+ const cartId = result?.cart?.id
+ const headers = cartId ? cart.setCartId(result.cart.id) : new Headers()
+ const { cart: cartResult, errors } = result
+
+ const redirectTo = formData.get('redirectTo') ?? null
+ if (typeof redirectTo === 'string') {
+ status = 303
+ headers.set('Location', redirectTo)
+ }
+
+ return json(
+ {
+ cart: cartResult,
+ errors,
+ analytics: {
+ cartId,
+ },
+ },
+ { status, headers },
+ )
+}
+
+export default function Cart() {
+ const rootData = useRouteLoaderData('root')
+ if (!rootData) return null
+
+ return (
+
+
Cart
+
Loading cart ...}>
+ An error occurred }>
+ {(cart) => {
+ return
+ }}
+
+
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections.$handle.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections.$handle.tsx
new file mode 100644
index 000000000..dbef15100
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections.$handle.tsx
@@ -0,0 +1,180 @@
+import { defer, redirect, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, Link, type MetaFunction } from '@remix-run/react'
+import { getPaginationVariables, Image, Money, Analytics } from '@shopify/hydrogen'
+import type { ProductItemFragment } from 'storefrontapi.generated'
+import { useVariantUrl } from '~/lib/variants'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.collection.title ?? ''} Collection` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, params, request }: LoaderFunctionArgs) {
+ const { handle } = params
+ const { storefront } = context
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 8,
+ })
+
+ if (!handle) {
+ throw redirect('/collections')
+ }
+
+ const [{ collection }] = await Promise.all([
+ storefront.query(COLLECTION_QUERY, {
+ variables: { handle, ...paginationVariables },
+ // Add other queries here, so that they are loaded in parallel
+ }),
+ ])
+
+ if (!collection) {
+ throw new Response(`Collection ${handle} not found`, {
+ status: 404,
+ })
+ }
+
+ return {
+ collection,
+ }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Collection() {
+ const { collection } = useLoaderData()
+
+ return (
+
+
{collection.title}
+
{collection.description}
+
+ {({ node: product, index }) => (
+
+ )}
+
+
+
+ )
+}
+
+function ProductItem({ product, loading }: { product: ProductItemFragment; loading?: 'eager' | 'lazy' }) {
+ const variant = product.variants.nodes[0]
+ const variantUrl = useVariantUrl(product.handle, variant.selectedOptions)
+ return (
+
+ {product.featuredImage && (
+
+ )}
+ {product.title}
+
+
+
+
+ )
+}
+
+const PRODUCT_ITEM_FRAGMENT = `#graphql
+ fragment MoneyProductItem on MoneyV2 {
+ amount
+ currencyCode
+ }
+ fragment ProductItem on Product {
+ id
+ handle
+ title
+ featuredImage {
+ id
+ altText
+ url
+ width
+ height
+ }
+ priceRange {
+ minVariantPrice {
+ ...MoneyProductItem
+ }
+ maxVariantPrice {
+ ...MoneyProductItem
+ }
+ }
+ variants(first: 1) {
+ nodes {
+ selectedOptions {
+ name
+ value
+ }
+ }
+ }
+ }
+` as const
+
+// NOTE: https://shopify.dev/docs/api/storefront/2022-04/objects/collection
+const COLLECTION_QUERY = `#graphql
+ ${PRODUCT_ITEM_FRAGMENT}
+ query Collection(
+ $handle: String!
+ $country: CountryCode
+ $language: LanguageCode
+ $first: Int
+ $last: Int
+ $startCursor: String
+ $endCursor: String
+ ) @inContext(country: $country, language: $language) {
+ collection(handle: $handle) {
+ id
+ handle
+ title
+ description
+ products(
+ first: $first,
+ last: $last,
+ before: $startCursor,
+ after: $endCursor
+ ) {
+ nodes {
+ ...ProductItem
+ }
+ pageInfo {
+ hasPreviousPage
+ hasNextPage
+ endCursor
+ startCursor
+ }
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections._index.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections._index.tsx
new file mode 100644
index 000000000..b24ea672a
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections._index.tsx
@@ -0,0 +1,112 @@
+import { useLoaderData, Link } from '@remix-run/react'
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { getPaginationVariables, Image } from '@shopify/hydrogen'
+import type { CollectionFragment } from 'storefrontapi.generated'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, request }: LoaderFunctionArgs) {
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 4,
+ })
+
+ const [{ collections }] = await Promise.all([
+ context.storefront.query(COLLECTIONS_QUERY, {
+ variables: paginationVariables,
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ return { collections }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Collections() {
+ const { collections } = useLoaderData()
+
+ return (
+
+
Collections
+
+ {({ node: collection, index }) => }
+
+
+ )
+}
+
+function CollectionItem({ collection, index }: { collection: CollectionFragment; index: number }) {
+ return (
+
+ {collection?.image && (
+
+ )}
+ {collection.title}
+
+ )
+}
+
+const COLLECTIONS_QUERY = `#graphql
+ fragment Collection on Collection {
+ id
+ title
+ handle
+ image {
+ id
+ url
+ altText
+ width
+ height
+ }
+ }
+ query StoreCollections(
+ $country: CountryCode
+ $endCursor: String
+ $first: Int
+ $language: LanguageCode
+ $last: Int
+ $startCursor: String
+ ) @inContext(country: $country, language: $language) {
+ collections(
+ first: $first,
+ last: $last,
+ before: $startCursor,
+ after: $endCursor
+ ) {
+ nodes {
+ ...Collection
+ }
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections.all.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections.all.tsx
new file mode 100644
index 000000000..92dc49036
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/collections.all.tsx
@@ -0,0 +1,145 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, Link, type MetaFunction } from '@remix-run/react'
+import { getPaginationVariables, Image, Money } from '@shopify/hydrogen'
+import type { ProductItemFragment } from 'storefrontapi.generated'
+import { useVariantUrl } from '~/lib/variants'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export const meta: MetaFunction = () => {
+ return [{ title: `Hydrogen | Products` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, request }: LoaderFunctionArgs) {
+ const { storefront } = context
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 8,
+ })
+
+ const [{ products }] = await Promise.all([
+ storefront.query(CATALOG_QUERY, {
+ variables: { ...paginationVariables },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+ return { products }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Collection() {
+ const { products } = useLoaderData()
+
+ return (
+
+
Products
+
+ {({ node: product, index }) => (
+
+ )}
+
+
+ )
+}
+
+function ProductItem({ product, loading }: { product: ProductItemFragment; loading?: 'eager' | 'lazy' }) {
+ const variant = product.variants.nodes[0]
+ const variantUrl = useVariantUrl(product.handle, variant.selectedOptions)
+ return (
+
+ {product.featuredImage && (
+
+ )}
+ {product.title}
+
+
+
+
+ )
+}
+
+const PRODUCT_ITEM_FRAGMENT = `#graphql
+ fragment MoneyProductItem on MoneyV2 {
+ amount
+ currencyCode
+ }
+ fragment ProductItem on Product {
+ id
+ handle
+ title
+ featuredImage {
+ id
+ altText
+ url
+ width
+ height
+ }
+ priceRange {
+ minVariantPrice {
+ ...MoneyProductItem
+ }
+ maxVariantPrice {
+ ...MoneyProductItem
+ }
+ }
+ variants(first: 1) {
+ nodes {
+ selectedOptions {
+ name
+ value
+ }
+ }
+ }
+ }
+` as const
+
+// NOTE: https://shopify.dev/docs/api/storefront/2024-01/objects/product
+const CATALOG_QUERY = `#graphql
+ query Catalog(
+ $country: CountryCode
+ $language: LanguageCode
+ $first: Int
+ $last: Int
+ $startCursor: String
+ $endCursor: String
+ ) @inContext(country: $country, language: $language) {
+ products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
+ nodes {
+ ...ProductItem
+ }
+ pageInfo {
+ hasPreviousPage
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ }
+ }
+ ${PRODUCT_ITEM_FRAGMENT}
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/discount.$code.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/discount.$code.tsx
new file mode 100644
index 000000000..0decae16e
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/discount.$code.tsx
@@ -0,0 +1,46 @@
+import { redirect, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+
+/**
+ * Automatically applies a discount found on the url
+ * If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
+ *
+ * @example
+ * Example path applying a discount and optional redirecting (defaults to the home page)
+ * ```js
+ * /discount/FREESHIPPING?redirect=/products
+ *
+ * ```
+ */
+export async function loader({ request, context, params }: LoaderFunctionArgs) {
+ const { cart } = context
+ const { code } = params
+
+ const url = new URL(request.url)
+ const searchParams = new URLSearchParams(url.search)
+ let redirectParam = searchParams.get('redirect') || searchParams.get('return_to') || '/'
+
+ if (redirectParam.includes('//')) {
+ // Avoid redirecting to external URLs to prevent phishing attacks
+ redirectParam = '/'
+ }
+
+ searchParams.delete('redirect')
+ searchParams.delete('return_to')
+
+ const redirectUrl = `${redirectParam}?${searchParams}`
+
+ if (!code) {
+ return redirect(redirectUrl)
+ }
+
+ const result = await cart.updateDiscountCodes([code])
+ const headers = cart.setCartId(result.cart.id)
+
+ // Using set-cookie on a 303 redirect will not work if the domain origin have port number (:3000)
+ // If there is no cart id and a new cart id is created in the progress, it will not be set in the cookie
+ // on localhost:3000
+ return redirect(redirectUrl, {
+ status: 303,
+ headers,
+ })
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/pages.$handle.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/pages.$handle.tsx
new file mode 100644
index 000000000..c9a0956f7
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/pages.$handle.tsx
@@ -0,0 +1,84 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, type MetaFunction } from '@remix-run/react'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.page.title ?? ''}` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, params }: LoaderFunctionArgs) {
+ if (!params.handle) {
+ throw new Error('Missing page handle')
+ }
+
+ const [{ page }] = await Promise.all([
+ context.storefront.query(PAGE_QUERY, {
+ variables: {
+ handle: params.handle,
+ },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ if (!page) {
+ throw new Response('Not Found', { status: 404 })
+ }
+
+ return {
+ page,
+ }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Page() {
+ const { page } = useLoaderData()
+
+ return (
+
+
+
+
+ )
+}
+
+const PAGE_QUERY = `#graphql
+ query Page(
+ $language: LanguageCode,
+ $country: CountryCode,
+ $handle: String!
+ )
+ @inContext(language: $language, country: $country) {
+ page(handle: $handle) {
+ id
+ title
+ body
+ seo {
+ description
+ title
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/policies.$handle.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/policies.$handle.tsx
new file mode 100644
index 000000000..1ddece7d4
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/policies.$handle.tsx
@@ -0,0 +1,89 @@
+import { json, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { Link, useLoaderData, type MetaFunction } from '@remix-run/react'
+import { type Shop } from '@shopify/hydrogen/storefront-api-types'
+
+type SelectedPolicies = keyof Pick
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.policy.title ?? ''}` }]
+}
+
+export async function loader({ params, context }: LoaderFunctionArgs) {
+ if (!params.handle) {
+ throw new Response('No handle was passed in', { status: 404 })
+ }
+
+ const policyName = params.handle.replace(/-([a-z])/g, (_: unknown, m1: string) =>
+ m1.toUpperCase(),
+ ) as SelectedPolicies
+
+ const data = await context.storefront.query(POLICY_CONTENT_QUERY, {
+ variables: {
+ privacyPolicy: false,
+ shippingPolicy: false,
+ termsOfService: false,
+ refundPolicy: false,
+ [policyName]: true,
+ language: context.storefront.i18n?.language,
+ },
+ })
+
+ const policy = data.shop?.[policyName]
+
+ if (!policy) {
+ throw new Response('Could not find the policy', { status: 404 })
+ }
+
+ return json({ policy })
+}
+
+export default function Policy() {
+ const { policy } = useLoaderData()
+
+ return (
+
+
+
+
+ ← Back to Policies
+
+
+
{policy.title}
+
+
+ )
+}
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/Shop
+const POLICY_CONTENT_QUERY = `#graphql
+ fragment Policy on ShopPolicy {
+ body
+ handle
+ id
+ title
+ url
+ }
+ query Policy(
+ $country: CountryCode
+ $language: LanguageCode
+ $privacyPolicy: Boolean!
+ $refundPolicy: Boolean!
+ $shippingPolicy: Boolean!
+ $termsOfService: Boolean!
+ ) @inContext(language: $language, country: $country) {
+ shop {
+ privacyPolicy @include(if: $privacyPolicy) {
+ ...Policy
+ }
+ shippingPolicy @include(if: $shippingPolicy) {
+ ...Policy
+ }
+ termsOfService @include(if: $termsOfService) {
+ ...Policy
+ }
+ refundPolicy @include(if: $refundPolicy) {
+ ...Policy
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/policies._index.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/policies._index.tsx
new file mode 100644
index 000000000..f7a71e0c9
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/policies._index.tsx
@@ -0,0 +1,63 @@
+import { json, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, Link } from '@remix-run/react'
+
+export async function loader({ context }: LoaderFunctionArgs) {
+ const data = await context.storefront.query(POLICIES_QUERY)
+ const policies = Object.values(data.shop || {})
+
+ if (!policies.length) {
+ throw new Response('No policies found', { status: 404 })
+ }
+
+ return json({ policies })
+}
+
+export default function Policies() {
+ const { policies } = useLoaderData()
+
+ return (
+
+
Policies
+
+ {policies.map((policy) => {
+ if (!policy) return null
+ return (
+
+ {policy.title}
+
+ )
+ })}
+
+
+ )
+}
+
+const POLICIES_QUERY = `#graphql
+ fragment PolicyItem on ShopPolicy {
+ id
+ title
+ handle
+ }
+ query Policies ($country: CountryCode, $language: LanguageCode)
+ @inContext(country: $country, language: $language) {
+ shop {
+ privacyPolicy {
+ ...PolicyItem
+ }
+ shippingPolicy {
+ ...PolicyItem
+ }
+ termsOfService {
+ ...PolicyItem
+ }
+ refundPolicy {
+ ...PolicyItem
+ }
+ subscriptionPolicy {
+ id
+ title
+ handle
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/products.$handle.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/products.$handle.tsx
new file mode 100644
index 000000000..46bb64f56
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/products.$handle.tsx
@@ -0,0 +1,267 @@
+import { Suspense } from 'react'
+import { defer, redirect, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { Await, useLoaderData, type MetaFunction } from '@remix-run/react'
+import type { ProductFragment } from 'storefrontapi.generated'
+import { getSelectedProductOptions, Analytics, useOptimisticVariant } from '@shopify/hydrogen'
+import type { SelectedOption } from '@shopify/hydrogen/storefront-api-types'
+import { getVariantUrl } from '~/lib/variants'
+import { ProductPrice } from '~/components/ProductPrice'
+import { ProductImage } from '~/components/ProductImage'
+import { ProductForm } from '~/components/ProductForm'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.product.title ?? ''}` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, params, request }: LoaderFunctionArgs) {
+ const { handle } = params
+ const { storefront } = context
+
+ if (!handle) {
+ throw new Error('Expected product handle to be defined')
+ }
+
+ const [{ product }] = await Promise.all([
+ storefront.query(PRODUCT_QUERY, {
+ variables: { handle, selectedOptions: getSelectedProductOptions(request) },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ if (!product?.id) {
+ throw new Response(null, { status: 404 })
+ }
+
+ const firstVariant = product.variants.nodes[0]
+ const firstVariantIsDefault = Boolean(
+ firstVariant.selectedOptions.find(
+ (option: SelectedOption) => option.name === 'Title' && option.value === 'Default Title',
+ ),
+ )
+
+ if (firstVariantIsDefault) {
+ product.selectedVariant = firstVariant
+ } else {
+ // if no selected variant was returned from the selected options,
+ // we redirect to the first variant's url with it's selected options applied
+ if (!product.selectedVariant) {
+ throw redirectToFirstVariant({ product, request })
+ }
+ }
+
+ return {
+ product,
+ }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context, params }: LoaderFunctionArgs) {
+ // In order to show which variants are available in the UI, we need to query
+ // all of them. But there might be a *lot*, so instead separate the variants
+ // into it's own separate query that is deferred. So there's a brief moment
+ // where variant options might show as available when they're not, but after
+ // this deffered query resolves, the UI will update.
+ const variants = context.storefront
+ .query(VARIANTS_QUERY, {
+ variables: { handle: params.handle! },
+ })
+ .catch((error) => {
+ // Log query errors, but don't throw them so the page can still render
+ console.error(error)
+ return null
+ })
+
+ return {
+ variants,
+ }
+}
+
+function redirectToFirstVariant({ product, request }: { product: ProductFragment; request: Request }) {
+ const url = new URL(request.url)
+ const firstVariant = product.variants.nodes[0]
+
+ return redirect(
+ getVariantUrl({
+ pathname: url.pathname,
+ handle: product.handle,
+ selectedOptions: firstVariant.selectedOptions,
+ searchParams: new URLSearchParams(url.search),
+ }),
+ {
+ status: 302,
+ },
+ )
+}
+
+export default function Product() {
+ const { product, variants } = useLoaderData()
+ const selectedVariant = useOptimisticVariant(product.selectedVariant, variants)
+
+ const { title, descriptionHtml } = product
+
+ return (
+
+
+
+
{title}
+
+
+
}>
+
+ {(data) => (
+
+ )}
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+ )
+}
+
+const PRODUCT_VARIANT_FRAGMENT = `#graphql
+ fragment ProductVariant on ProductVariant {
+ availableForSale
+ compareAtPrice {
+ amount
+ currencyCode
+ }
+ id
+ image {
+ __typename
+ id
+ url
+ altText
+ width
+ height
+ }
+ price {
+ amount
+ currencyCode
+ }
+ product {
+ title
+ handle
+ }
+ selectedOptions {
+ name
+ value
+ }
+ sku
+ title
+ unitPrice {
+ amount
+ currencyCode
+ }
+ }
+` as const
+
+const PRODUCT_FRAGMENT = `#graphql
+ fragment Product on Product {
+ id
+ title
+ vendor
+ handle
+ descriptionHtml
+ description
+ options {
+ name
+ values
+ }
+ selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
+ ...ProductVariant
+ }
+ variants(first: 1) {
+ nodes {
+ ...ProductVariant
+ }
+ }
+ seo {
+ description
+ title
+ }
+ }
+ ${PRODUCT_VARIANT_FRAGMENT}
+` as const
+
+const PRODUCT_QUERY = `#graphql
+ query Product(
+ $country: CountryCode
+ $handle: String!
+ $language: LanguageCode
+ $selectedOptions: [SelectedOptionInput!]!
+ ) @inContext(country: $country, language: $language) {
+ product(handle: $handle) {
+ ...Product
+ }
+ }
+ ${PRODUCT_FRAGMENT}
+` as const
+
+const PRODUCT_VARIANTS_FRAGMENT = `#graphql
+ fragment ProductVariants on Product {
+ variants(first: 250) {
+ nodes {
+ ...ProductVariant
+ }
+ }
+ }
+ ${PRODUCT_VARIANT_FRAGMENT}
+` as const
+
+const VARIANTS_QUERY = `#graphql
+ ${PRODUCT_VARIANTS_FRAGMENT}
+ query ProductVariants(
+ $country: CountryCode
+ $language: LanguageCode
+ $handle: String!
+ ) @inContext(country: $country, language: $language) {
+ product(handle: $handle) {
+ ...ProductVariants
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/search.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/search.tsx
new file mode 100644
index 000000000..0dfd3982b
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/search.tsx
@@ -0,0 +1,381 @@
+import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, type MetaFunction } from '@remix-run/react'
+import { getPaginationVariables, Analytics } from '@shopify/hydrogen'
+import { SearchForm } from '~/components/SearchForm'
+import { SearchResults } from '~/components/SearchResults'
+import { type RegularSearchReturn, type PredictiveSearchReturn, getEmptyPredictiveSearchResult } from '~/lib/search'
+
+export const meta: MetaFunction = () => {
+ return [{ title: `Hydrogen | Search` }]
+}
+
+export async function loader({ request, context }: LoaderFunctionArgs) {
+ const url = new URL(request.url)
+ const isPredictive = url.searchParams.has('predictive')
+ const searchPromise = isPredictive ? predictiveSearch({ request, context }) : regularSearch({ request, context })
+
+ searchPromise.catch((error: Error) => {
+ console.error(error)
+ return { term: '', result: null, error: error.message }
+ })
+
+ return json(await searchPromise)
+}
+
+/**
+ * Renders the /search route
+ */
+export default function SearchPage() {
+ const { type, term, result, error } = useLoaderData()
+ if (type === 'predictive') return null
+
+ return (
+
+
Search
+
+ {({ inputRef }) => (
+ <>
+
+
+ Search
+ >
+ )}
+
+ {error &&
{error}
}
+ {!term || !result?.total ? (
+
+ ) : (
+
+ {({ articles, pages, products, term }) => (
+
+
+
+
+
+ )}
+
+ )}
+
+
+ )
+}
+
+/**
+ * Regular search query and fragments
+ * (adjust as needed)
+ */
+const SEARCH_PRODUCT_FRAGMENT = `#graphql
+ fragment SearchProduct on Product {
+ __typename
+ handle
+ id
+ publishedAt
+ title
+ trackingParameters
+ vendor
+ variants(first: 1) {
+ nodes {
+ id
+ image {
+ url
+ altText
+ width
+ height
+ }
+ price {
+ amount
+ currencyCode
+ }
+ compareAtPrice {
+ amount
+ currencyCode
+ }
+ selectedOptions {
+ name
+ value
+ }
+ product {
+ handle
+ title
+ }
+ }
+ }
+ }
+` as const
+
+const SEARCH_PAGE_FRAGMENT = `#graphql
+ fragment SearchPage on Page {
+ __typename
+ handle
+ id
+ title
+ trackingParameters
+ }
+` as const
+
+const SEARCH_ARTICLE_FRAGMENT = `#graphql
+ fragment SearchArticle on Article {
+ __typename
+ handle
+ id
+ title
+ trackingParameters
+ }
+` as const
+
+const PAGE_INFO_FRAGMENT = `#graphql
+ fragment PageInfoFragment on PageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
+` as const
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/search
+export const SEARCH_QUERY = `#graphql
+ query RegularSearch(
+ $country: CountryCode
+ $endCursor: String
+ $first: Int
+ $language: LanguageCode
+ $last: Int
+ $term: String!
+ $startCursor: String
+ ) @inContext(country: $country, language: $language) {
+ articles: search(
+ query: $term,
+ types: [ARTICLE],
+ first: $first,
+ ) {
+ nodes {
+ ...on Article {
+ ...SearchArticle
+ }
+ }
+ }
+ pages: search(
+ query: $term,
+ types: [PAGE],
+ first: $first,
+ ) {
+ nodes {
+ ...on Page {
+ ...SearchPage
+ }
+ }
+ }
+ products: search(
+ after: $endCursor,
+ before: $startCursor,
+ first: $first,
+ last: $last,
+ query: $term,
+ sortKey: RELEVANCE,
+ types: [PRODUCT],
+ unavailableProducts: HIDE,
+ ) {
+ nodes {
+ ...on Product {
+ ...SearchProduct
+ }
+ }
+ pageInfo {
+ ...PageInfoFragment
+ }
+ }
+ }
+ ${SEARCH_PRODUCT_FRAGMENT}
+ ${SEARCH_PAGE_FRAGMENT}
+ ${SEARCH_ARTICLE_FRAGMENT}
+ ${PAGE_INFO_FRAGMENT}
+` as const
+
+/**
+ * Regular search fetcher
+ */
+async function regularSearch({
+ request,
+ context,
+}: Pick): Promise {
+ const { storefront } = context
+ const url = new URL(request.url)
+ const variables = getPaginationVariables(request, { pageBy: 8 })
+ const term = String(url.searchParams.get('q') || '')
+
+ // Search articles, pages, and products for the `q` term
+ const { errors, ...items } = await storefront.query(SEARCH_QUERY, {
+ variables: { ...variables, term },
+ })
+
+ if (!items) {
+ throw new Error('No search data returned from Shopify API')
+ }
+
+ const total = Object.values(items).reduce((acc, { nodes }) => acc + nodes.length, 0)
+
+ const error = errors ? errors.map(({ message }) => message).join(', ') : undefined
+
+ return { type: 'regular', term, error, result: { total, items } }
+}
+
+/**
+ * Predictive search query and fragments
+ * (adjust as needed)
+ */
+const PREDICTIVE_SEARCH_ARTICLE_FRAGMENT = `#graphql
+ fragment PredictiveArticle on Article {
+ __typename
+ id
+ title
+ handle
+ blog {
+ handle
+ }
+ image {
+ url
+ altText
+ width
+ height
+ }
+ trackingParameters
+ }
+` as const
+
+const PREDICTIVE_SEARCH_COLLECTION_FRAGMENT = `#graphql
+ fragment PredictiveCollection on Collection {
+ __typename
+ id
+ title
+ handle
+ image {
+ url
+ altText
+ width
+ height
+ }
+ trackingParameters
+ }
+` as const
+
+const PREDICTIVE_SEARCH_PAGE_FRAGMENT = `#graphql
+ fragment PredictivePage on Page {
+ __typename
+ id
+ title
+ handle
+ trackingParameters
+ }
+` as const
+
+const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql
+ fragment PredictiveProduct on Product {
+ __typename
+ id
+ title
+ handle
+ trackingParameters
+ variants(first: 1) {
+ nodes {
+ id
+ image {
+ url
+ altText
+ width
+ height
+ }
+ price {
+ amount
+ currencyCode
+ }
+ }
+ }
+ }
+` as const
+
+const PREDICTIVE_SEARCH_QUERY_FRAGMENT = `#graphql
+ fragment PredictiveQuery on SearchQuerySuggestion {
+ __typename
+ text
+ styledText
+ trackingParameters
+ }
+` as const
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/predictiveSearch
+const PREDICTIVE_SEARCH_QUERY = `#graphql
+ query PredictiveSearch(
+ $country: CountryCode
+ $language: LanguageCode
+ $limit: Int!
+ $limitScope: PredictiveSearchLimitScope!
+ $term: String!
+ $types: [PredictiveSearchType!]
+ ) @inContext(country: $country, language: $language) {
+ predictiveSearch(
+ limit: $limit,
+ limitScope: $limitScope,
+ query: $term,
+ types: $types,
+ ) {
+ articles {
+ ...PredictiveArticle
+ }
+ collections {
+ ...PredictiveCollection
+ }
+ pages {
+ ...PredictivePage
+ }
+ products {
+ ...PredictiveProduct
+ }
+ queries {
+ ...PredictiveQuery
+ }
+ }
+ }
+ ${PREDICTIVE_SEARCH_ARTICLE_FRAGMENT}
+ ${PREDICTIVE_SEARCH_COLLECTION_FRAGMENT}
+ ${PREDICTIVE_SEARCH_PAGE_FRAGMENT}
+ ${PREDICTIVE_SEARCH_PRODUCT_FRAGMENT}
+ ${PREDICTIVE_SEARCH_QUERY_FRAGMENT}
+` as const
+
+/**
+ * Predictive search fetcher
+ */
+async function predictiveSearch({
+ request,
+ context,
+}: Pick): Promise {
+ const { storefront } = context
+ const url = new URL(request.url)
+ const term = String(url.searchParams.get('q') || '').trim()
+ const limit = Number(url.searchParams.get('limit') || 10)
+ const type = 'predictive'
+
+ if (!term) return { type, term, result: getEmptyPredictiveSearchResult() }
+
+ // Predictively search articles, collections, pages, products, and queries (suggestions)
+ const { predictiveSearch: items, errors } = await storefront.query(PREDICTIVE_SEARCH_QUERY, {
+ variables: {
+ // customize search options as needed
+ limit,
+ limitScope: 'EACH',
+ term,
+ },
+ })
+
+ if (errors) {
+ throw new Error(`Shopify API errors: ${errors.map(({ message }) => message).join(', ')}`)
+ }
+
+ if (!items) {
+ throw new Error('No predictive search data returned from Shopify API')
+ }
+
+ const total = Object.values(items).reduce((acc, item) => acc + item.length, 0)
+
+ return { type, term, result: { items, total } }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/styles/app.css b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/styles/app.css
new file mode 100644
index 000000000..c9938e4d5
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/styles/app.css
@@ -0,0 +1,483 @@
+:root {
+ --aside-width: 400px;
+ --cart-aside-summary-height-with-discount: 300px;
+ --cart-aside-summary-height: 250px;
+ --grid-item-width: 355px;
+ --header-height: 64px;
+ --color-dark: #000;
+ --color-light: #fff;
+}
+
+img {
+ border-radius: 4px;
+}
+
+/*
+* --------------------------------------------------
+* components/Aside
+* --------------------------------------------------
+*/
+aside {
+ background: var(--color-light);
+ box-shadow: 0 0 50px rgba(0, 0, 0, 0.3);
+ height: 100vh;
+ max-width: var(--aside-width);
+ min-width: var(--aside-width);
+ position: fixed;
+ right: calc(-1 * var(--aside-width));
+ top: 0;
+ transition: transform 200ms ease-in-out;
+}
+
+aside header {
+ align-items: center;
+ border-bottom: 1px solid var(--color-dark);
+ display: flex;
+ height: var(--header-height);
+ justify-content: space-between;
+ padding: 0 20px;
+}
+
+aside header h3 {
+ margin: 0;
+}
+
+aside header .close {
+ font-weight: bold;
+ opacity: 0.8;
+ text-decoration: none;
+ transition: all 200ms;
+ width: 20px;
+}
+
+aside header .close:hover {
+ opacity: 1;
+}
+
+aside header h2 {
+ margin-bottom: 0.6rem;
+ margin-top: 0;
+}
+
+aside main {
+ margin: 1rem;
+}
+
+aside p {
+ margin: 0 0 0.25rem;
+}
+
+aside p:last-child {
+ margin: 0;
+}
+
+aside li {
+ margin-bottom: 0.125rem;
+}
+
+.overlay {
+ background: rgba(0, 0, 0, 0.2);
+ bottom: 0;
+ left: 0;
+ opacity: 0;
+ pointer-events: none;
+ position: fixed;
+ right: 0;
+ top: 0;
+ transition: opacity 400ms ease-in-out;
+ transition: opacity 400ms;
+ visibility: hidden;
+ z-index: 10;
+}
+
+.overlay .close-outside {
+ background: transparent;
+ border: none;
+ color: transparent;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: calc(100% - var(--aside-width));
+}
+
+.overlay .light {
+ background: rgba(255, 255, 255, 0.5);
+}
+
+.overlay .cancel {
+ cursor: default;
+ height: 100%;
+ position: absolute;
+ width: 100%;
+}
+
+.overlay.expanded {
+ opacity: 1;
+ pointer-events: auto;
+ visibility: visible;
+}
+
+/* reveal aside */
+.overlay.expanded aside {
+ transform: translateX(calc(var(--aside-width) * -1));
+}
+
+button.reset {
+ border: 0;
+ background: inherit;
+ font-size: inherit;
+}
+
+button.reset > * {
+ margin: 0;
+}
+
+button.reset:not(:has(> *)) {
+ height: 1.5rem;
+ line-height: 1.5rem;
+}
+
+button.reset:hover:not(:has(> *)) {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+/*
+* --------------------------------------------------
+* components/Header
+* --------------------------------------------------
+*/
+.header {
+ align-items: center;
+ background: #fff;
+ display: flex;
+ height: var(--header-height);
+ padding: 0 1rem;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.header-menu-mobile-toggle {
+ @media (min-width: 48em) {
+ display: none;
+ }
+}
+
+.header-menu-mobile {
+ display: flex;
+ flex-direction: column;
+ grid-gap: 1rem;
+}
+
+.header-menu-desktop {
+ display: none;
+ grid-gap: 1rem;
+
+ @media (min-width: 45em) {
+ display: flex;
+ grid-gap: 1rem;
+ margin-left: 3rem;
+ }
+}
+
+.header-menu-item {
+ cursor: pointer;
+}
+
+.header-ctas {
+ align-items: center;
+ display: flex;
+ grid-gap: 1rem;
+ margin-left: auto;
+}
+
+/*
+* --------------------------------------------------
+* components/Footer
+* --------------------------------------------------
+*/
+.footer {
+ background: var(--color-dark);
+ margin-top: auto;
+}
+
+.footer-menu {
+ align-items: center;
+ display: flex;
+ grid-gap: 1rem;
+ padding: 1rem;
+}
+
+.footer-menu a {
+ color: var(--color-light);
+}
+
+/*
+* --------------------------------------------------
+* components/Cart
+* --------------------------------------------------
+*/
+.cart-main {
+ height: 100%;
+ max-height: calc(100vh - var(--cart-aside-summary-height));
+ overflow-y: auto;
+ width: auto;
+}
+
+.cart-main.with-discount {
+ max-height: calc(100vh - var(--cart-aside-summary-height-with-discount));
+}
+
+.cart-line {
+ display: flex;
+ padding: 0.75rem 0;
+}
+
+.cart-line img {
+ height: 100%;
+ display: block;
+ margin-right: 0.75rem;
+}
+
+.cart-summary-page {
+ position: relative;
+}
+
+.cart-summary-aside {
+ background: white;
+ border-top: 1px solid var(--color-dark);
+ bottom: 0;
+ padding-top: 0.75rem;
+ position: absolute;
+ width: calc(var(--aside-width) - 40px);
+}
+
+.cart-line-quantity {
+ display: flex;
+}
+
+.cart-discount {
+ align-items: center;
+ display: flex;
+ margin-top: 0.25rem;
+}
+
+.cart-subtotal {
+ align-items: center;
+ display: flex;
+}
+
+/*
+* --------------------------------------------------
+* components/Search
+* --------------------------------------------------
+*/
+.predictive-search {
+ height: calc(100vh - var(--header-height) - 40px);
+ overflow-y: auto;
+}
+
+.predictive-search-form {
+ background: var(--color-light);
+ position: sticky;
+ top: 0;
+}
+
+.predictive-search-result {
+ margin-bottom: 2rem;
+}
+
+.predictive-search-result h5 {
+ text-transform: uppercase;
+}
+
+.predictive-search-result-item {
+ margin-bottom: 0.5rem;
+}
+
+.predictive-search-result-item a {
+ align-items: center;
+ display: flex;
+}
+
+.predictive-search-result-item a img {
+ margin-right: 0.75rem;
+ height: 100%;
+}
+
+.search-result {
+ margin-bottom: 1.5rem;
+}
+
+.search-results-item {
+ margin-bottom: 0.5rem;
+}
+
+.search-results-item a {
+ display: flex;
+ flex: row;
+ align-items: center;
+ gap: 1rem;
+}
+
+/*
+* --------------------------------------------------
+* routes/__index
+* --------------------------------------------------
+*/
+.featured-collection {
+ display: block;
+ margin-bottom: 2rem;
+ position: relative;
+}
+
+.featured-collection-image {
+ aspect-ratio: 1 / 1;
+
+ @media (min-width: 45em) {
+ aspect-ratio: 16 / 9;
+ }
+}
+
+.featured-collection img {
+ height: auto;
+ max-height: 100%;
+ object-fit: cover;
+}
+
+.recommended-products-grid {
+ display: grid;
+ grid-gap: 1.5rem;
+ grid-template-columns: repeat(2, 1fr);
+
+ @media (min-width: 45em) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+}
+
+.recommended-product img {
+ height: auto;
+}
+
+/*
+* --------------------------------------------------
+* routes/collections._index.tsx
+* --------------------------------------------------
+*/
+.collections-grid {
+ display: grid;
+ grid-gap: 1.5rem;
+ grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
+ margin-bottom: 2rem;
+}
+
+.collection-item img {
+ height: auto;
+}
+
+/*
+* --------------------------------------------------
+* routes/collections.$handle.tsx
+* --------------------------------------------------
+*/
+.collection-description {
+ margin-bottom: 1rem;
+ max-width: 95%;
+
+ @media (min-width: 45em) {
+ max-width: 600px;
+ }
+}
+
+.products-grid {
+ display: grid;
+ grid-gap: 1.5rem;
+ grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
+ margin-bottom: 2rem;
+}
+
+.product-item img {
+ height: auto;
+ width: 100%;
+}
+
+/*
+* --------------------------------------------------
+* routes/products.$handle.tsx
+* --------------------------------------------------
+*/
+.product {
+ display: grid;
+
+ @media (min-width: 45em) {
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 4rem;
+ }
+}
+
+.product h1 {
+ margin-top: 0;
+}
+
+.product-image img {
+ height: auto;
+ width: 100%;
+}
+
+.product-main {
+ align-self: start;
+ position: sticky;
+ top: 6rem;
+}
+
+.product-price-on-sale {
+ display: flex;
+ grid-gap: 0.5rem;
+}
+
+.product-price-on-sale s {
+ opacity: 0.5;
+}
+
+.product-options-grid {
+ display: flex;
+ flex-wrap: wrap;
+ grid-gap: 0.75rem;
+}
+
+.product-options-item {
+ padding: 0.25rem 0.5rem;
+}
+
+/*
+* --------------------------------------------------
+* routes/blog._index.tsx
+* --------------------------------------------------
+*/
+.blog-grid {
+ display: grid;
+ grid-gap: 1.5rem;
+ grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
+ margin-bottom: 2rem;
+}
+
+.blog-article-image {
+ aspect-ratio: 3/2;
+ display: block;
+}
+
+.blog-article-image img {
+ height: 100%;
+}
+
+/*
+* --------------------------------------------------
+* routes/blog.$articlehandle.tsx
+* --------------------------------------------------
+*/
+.article img {
+ height: auto;
+ width: 100%;
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/styles/reset.css b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/styles/reset.css
new file mode 100644
index 000000000..4488766b3
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/styles/reset.css
@@ -0,0 +1,139 @@
+body {
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ 'Open Sans',
+ 'Helvetica Neue',
+ sans-serif;
+ margin: 0;
+ padding: 0;
+}
+
+h1,
+h2,
+p {
+ margin: 0;
+ padding: 0;
+}
+
+h1 {
+ font-size: 1.6rem;
+ font-weight: 700;
+ line-height: 1.4;
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+}
+
+h2 {
+ font-size: 1.2rem;
+ font-weight: 700;
+ line-height: 1.4;
+ margin-bottom: 1rem;
+}
+
+h4 {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+h5 {
+ margin-bottom: 1rem;
+ margin-top: 0.5rem;
+}
+
+p {
+ font-size: 1rem;
+ line-height: 1.4;
+}
+
+a {
+ color: #000;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+hr {
+ border-bottom: none;
+ border-top: 1px solid #000;
+ margin: 0;
+}
+
+pre {
+ white-space: pre-wrap;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+body > main {
+ margin: 0 1rem 1rem 1rem;
+}
+
+section {
+ padding: 1rem 0;
+ @media (min-width: 768px) {
+ padding: 2rem 0;
+ }
+}
+
+fieldset {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 0.5rem;
+ padding: 1rem;
+}
+
+form {
+ max-width: 100%;
+ @media (min-width: 768px) {
+ max-width: 400px;
+ }
+}
+
+input {
+ border-radius: 4px;
+ border: 1px solid #000;
+ font-size: 1rem;
+ margin-bottom: 0.5rem;
+ margin-top: 0.25rem;
+ padding: 0.5rem;
+}
+
+legend {
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+li {
+ margin-bottom: 0.5rem;
+}
+
+dl {
+ margin: 0.5rem 0;
+}
+
+code {
+ background: #ddd;
+ border-radius: 4px;
+ font-family: monospace;
+ padding: 0.25rem;
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/env.d.ts b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/env.d.ts
new file mode 100644
index 000000000..f3e4328fc
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/env.d.ts
@@ -0,0 +1,28 @@
+///
+
+// Enhance TypeScript's built-in typings.
+import '@total-typescript/ts-reset'
+
+import type { HydrogenContext, HydrogenSessionData, HydrogenEnv } from '@shopify/hydrogen'
+import type { createAppLoadContext } from '~/lib/context'
+
+declare global {
+ /**
+ * A global `process` object is only available during build to access NODE_ENV.
+ */
+ const process: { env: { NODE_ENV: 'production' | 'development' } }
+
+ interface Env extends HydrogenEnv {
+ // declare additional Env parameter use in the fetch handler and Remix loader context here
+ }
+}
+
+declare module '@netlify/remix-runtime' {
+ interface AppLoadContext extends Awaited> {
+ // to change context type, change the return of createAppLoadContext() instead
+ }
+
+ interface SessionData extends HydrogenSessionData {
+ // declare local additions to the Remix session data here
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/netlify.toml b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/netlify.toml
new file mode 100644
index 000000000..462faa0e8
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/netlify.toml
@@ -0,0 +1,15 @@
+[build]
+ command = "npm run build"
+ publish = "dist/client"
+
+# Set immutable caching for static files, because they have fingerprinted filenames
+[[headers]]
+for = "/build/*"
+[headers.values]
+"Cache-Control" = "public, max-age=31560000, immutable"
+
+# These are only used to set up the template, and are not used in the build
+# If you want to update the real values, change them in the site UI or CLI
+[template.environment]
+PUBLIC_STORE_DOMAIN = "Store domain. Leave as 'mock.shop' to try the demo site"
+SESSION_SECRET = "Session secret - change to a random value for production"
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/package.json b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/package.json
new file mode 100644
index 000000000..1d9b3afef
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "hydrogen-storefront",
+ "private": true,
+ "sideEffects": false,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "build": "remix vite:build",
+ "codegen": "shopify hydrogen codegen",
+ "dev": "shopify hydrogen dev --codegen",
+ "lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx .",
+ "preview": "netlify serve",
+ "typecheck": "tsc"
+ },
+ "dependencies": {
+ "@netlify/edge-functions": "^2.10.0",
+ "@netlify/remix-edge-adapter": "^3.3.0",
+ "@netlify/remix-runtime": "^2.3.0",
+ "@remix-run/react": "^2.11.2",
+ "@shopify/hydrogen": "^2024.7.4",
+ "graphql": "^16.6.0",
+ "graphql-tag": "^2.12.6",
+ "isbot": "^5.1.17",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@graphql-codegen/cli": "^5.0.2",
+ "@remix-run/dev": "^2.11.2",
+ "@remix-run/eslint-config": "^2.11.2",
+ "@shopify/cli": "^3.66.1",
+ "@shopify/hydrogen-codegen": "^0.3.1",
+ "@shopify/prettier-config": "^1.1.2",
+ "@total-typescript/ts-reset": "^0.4.2",
+ "@types/eslint": "^8.4.10",
+ "@types/react": "^18.2.22",
+ "@types/react-dom": "^18.2.7",
+ "eslint": "^8.20.0",
+ "eslint-plugin-hydrogen": "0.12.2",
+ "prettier": "^2.8.4",
+ "typescript": "^5.2.2",
+ "vite": "^5.4.2",
+ "vite-tsconfig-paths": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/public/favicon.svg b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/public/favicon.svg
new file mode 100644
index 000000000..f6c649733
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/public/favicon.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/tsconfig.json b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/tsconfig.json
new file mode 100644
index 000000000..631280351
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "module": "ES2022",
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["app/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/vite.config.js b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/vite.config.js
new file mode 100644
index 000000000..c5d47daf9
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/vite.config.js
@@ -0,0 +1,41 @@
+import { defineConfig } from 'vite'
+import { hydrogen } from '@shopify/hydrogen/vite'
+import { netlifyPlugin } from '@netlify/remix-edge-adapter/plugin'
+import { vitePlugin as remix } from '@remix-run/dev'
+import tsconfigPaths from 'vite-tsconfig-paths'
+
+export default defineConfig({
+ plugins: [
+ hydrogen(),
+ remix({
+ presets: [hydrogen.preset()],
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ },
+ }),
+ netlifyPlugin(),
+ tsconfigPaths(),
+ ],
+ build: {
+ // Allow a strict Content-Security-Policy
+ // withtout inlining assets as base64:
+ assetsInlineLimit: 0,
+ },
+ ssr: {
+ optimizeDeps: {
+ /**
+ * Include dependencies here if they throw CJS<>ESM errors.
+ * For example, for the following error:
+ *
+ * > ReferenceError: module is not defined
+ * > at /Users/.../node_modules/example-dep/index.js:1:1
+ *
+ * Include 'example-dep' in the array below.
+ * @see https://vitejs.dev/config/dep-optimization-options
+ */
+ include: [],
+ },
+ },
+})
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/.graphqlrc.js b/tests/e2e/fixtures/hydrogen-vite-site/.graphqlrc.js
new file mode 100644
index 000000000..2ed3a7634
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/.graphqlrc.js
@@ -0,0 +1,24 @@
+import { getSchema } from '@shopify/hydrogen-codegen'
+
+/**
+ * GraphQL Config
+ * @see https://the-guild.dev/graphql/config/docs/user/usage
+ * @type {IGraphQLConfig}
+ */
+export default {
+ projects: {
+ default: {
+ schema: getSchema('storefront'),
+ documents: ['./*.{ts,tsx,js,jsx}', './app/**/*.{ts,tsx,js,jsx}', '!./app/graphql/**/*.{ts,tsx,js,jsx}'],
+ },
+
+ customer: {
+ schema: getSchema('customer-account'),
+ documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'],
+ },
+
+ // Add your own GraphQL projects here for CMS, Shopify Admin API, etc.
+ },
+}
+
+/** @typedef {import('graphql-config').IGraphQLConfig} IGraphQLConfig */
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/README.md b/tests/e2e/fixtures/hydrogen-vite-site/README.md
new file mode 100644
index 000000000..edbf6867f
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/README.md
@@ -0,0 +1,52 @@
+# Hydrogen template: Skeleton
+
+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.
+
+[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/netlify/hydrogen-template#SESSION_SECRET=mock%20token&PUBLIC_STORE_DOMAIN=mock.shop)
+
+- [Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
+- [Get familiar with Remix](https://remix.run/docs/)
+
+## What's included
+
+- Remix 2
+- Hydrogen
+- Shopify CLI
+- ESLint
+- Prettier
+- GraphQL generator
+- TypeScript and JavaScript flavors
+- Minimal setup of components and routes
+
+## Getting started
+
+**Requirements:**
+
+- Node.js version 18.0.0 or higher
+- Netlify CLI 17.0.0 or higher
+
+```bash
+npm install -g netlify-cli@latest
+```
+
+[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/netlify/hydrogen-template#SESSION_SECRET=mock%20token&PUBLIC_STORE_DOMAIN=mock.shop)
+
+To create a new project, either click the "Deploy to Netlify" button above, or run the following command:
+
+```bash
+npx create-remix@latest --template=netlify/hydrogen-template
+```
+
+## Local development
+
+```bash
+npm run dev
+```
+
+## Building for production
+
+```bash
+npm run build
+```
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/assets/favicon.svg b/tests/e2e/fixtures/hydrogen-vite-site/app/assets/favicon.svg
new file mode 100644
index 000000000..f6c649733
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/assets/favicon.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/AddToCartButton.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/AddToCartButton.tsx
new file mode 100644
index 000000000..5a941eb71
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/AddToCartButton.tsx
@@ -0,0 +1,29 @@
+import { type FetcherWithComponents } from '@remix-run/react'
+import { CartForm, type OptimisticCartLineInput } from '@shopify/hydrogen'
+
+export function AddToCartButton({
+ analytics,
+ children,
+ disabled,
+ lines,
+ onClick,
+}: {
+ analytics?: unknown
+ children: React.ReactNode
+ disabled?: boolean
+ lines: Array
+ onClick?: () => void
+}) {
+ return (
+
+ {(fetcher: FetcherWithComponents) => (
+ <>
+
+
+ {children}
+
+ >
+ )}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/Aside.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/Aside.tsx
new file mode 100644
index 000000000..2e9f1ed2d
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/Aside.tsx
@@ -0,0 +1,72 @@
+import { createContext, type ReactNode, useContext, useState } from 'react'
+
+type AsideType = 'search' | 'cart' | 'mobile' | 'closed'
+type AsideContextValue = {
+ type: AsideType
+ open: (mode: AsideType) => void
+ close: () => void
+}
+
+/**
+ * A side bar component with Overlay
+ * @example
+ * ```jsx
+ *
+ * ```
+ */
+export function Aside({
+ children,
+ heading,
+ type,
+}: {
+ children?: React.ReactNode
+ type: AsideType
+ heading: React.ReactNode
+}) {
+ const { type: activeType, close } = useAside()
+ const expanded = type === activeType
+
+ return (
+
+ )
+}
+
+const AsideContext = createContext(null)
+
+Aside.Provider = function AsideProvider({ children }: { children: ReactNode }) {
+ const [type, setType] = useState('closed')
+
+ return (
+ setType('closed'),
+ }}
+ >
+ {children}
+
+ )
+}
+
+export function useAside() {
+ const aside = useContext(AsideContext)
+ if (!aside) {
+ throw new Error('useAside must be used within an AsideProvider')
+ }
+ return aside
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartLineItem.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartLineItem.tsx
new file mode 100644
index 000000000..17e523d31
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartLineItem.tsx
@@ -0,0 +1,113 @@
+import type { CartLineUpdateInput } from '@shopify/hydrogen/storefront-api-types'
+import type { CartLayout } from '~/components/CartMain'
+import { CartForm, Image, type OptimisticCartLine } from '@shopify/hydrogen'
+import { useVariantUrl } from '~/lib/variants'
+import { Link } from '@remix-run/react'
+import { ProductPrice } from './ProductPrice'
+import { useAside } from './Aside'
+import type { CartApiQueryFragment } from 'storefrontapi.generated'
+
+type CartLine = OptimisticCartLine
+
+/**
+ * A single line item in the cart. It displays the product image, title, price.
+ * It also provides controls to update the quantity or remove the line item.
+ */
+export function CartLineItem({ layout, line }: { layout: CartLayout; line: CartLine }) {
+ const { id, merchandise } = line
+ const { product, title, image, selectedOptions } = merchandise
+ const lineItemUrl = useVariantUrl(product.handle, selectedOptions)
+ const { close } = useAside()
+
+ return (
+
+ {image && }
+
+
+
{
+ if (layout === 'aside') {
+ close()
+ }
+ }}
+ >
+
+ {product.title}
+
+
+
+
+ {selectedOptions.map((option) => (
+
+
+ {option.name}: {option.value}
+
+
+ ))}
+
+
+
+
+ )
+}
+
+/**
+ * Provides the controls to update the quantity of a line item in the cart.
+ * These controls are disabled when the line item is new, and the server
+ * hasn't yet responded that it was successfully added to the cart.
+ */
+function CartLineQuantity({ line }: { line: CartLine }) {
+ if (!line || typeof line?.quantity === 'undefined') return null
+ const { id: lineId, quantity, isOptimistic } = line
+ const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0))
+ const nextQuantity = Number((quantity + 1).toFixed(0))
+
+ return (
+
+ Quantity: {quantity}
+
+
+ −
+
+
+
+
+
+ +
+
+
+
+
+
+ )
+}
+
+/**
+ * A button that removes a line item from the cart. It is disabled
+ * when the line item is new, and the server hasn't yet responded
+ * that it was successfully added to the cart.
+ */
+function CartLineRemoveButton({ lineIds, disabled }: { lineIds: string[]; disabled: boolean }) {
+ return (
+
+
+ Remove
+
+
+ )
+}
+
+function CartLineUpdateButton({ children, lines }: { children: React.ReactNode; lines: CartLineUpdateInput[] }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartMain.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartMain.tsx
new file mode 100644
index 000000000..e076f93cf
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartMain.tsx
@@ -0,0 +1,58 @@
+import { useOptimisticCart } from '@shopify/hydrogen'
+import { Link } from '@remix-run/react'
+import type { CartApiQueryFragment } from 'storefrontapi.generated'
+import { useAside } from '~/components/Aside'
+import { CartLineItem } from '~/components/CartLineItem'
+import { CartSummary } from './CartSummary'
+
+export type CartLayout = 'page' | 'aside'
+
+export type CartMainProps = {
+ cart: CartApiQueryFragment | null
+ layout: CartLayout
+}
+
+/**
+ * The main cart component that displays the cart items and summary.
+ * It is used by both the /cart route and the cart aside dialog.
+ */
+export function CartMain({ layout, cart: originalCart }: CartMainProps) {
+ // The useOptimisticCart hook applies pending actions to the cart
+ // so the user immediately sees feedback when they modify the cart.
+ const cart = useOptimisticCart(originalCart)
+
+ const linesCount = Boolean(cart?.lines?.nodes?.length || 0)
+ const withDiscount = cart && Boolean(cart?.discountCodes?.filter((code) => code.applicable)?.length)
+ const className = `cart-main ${withDiscount ? 'with-discount' : ''}`
+ const cartHasItems = (cart?.totalQuantity ?? 0) > 0
+
+ return (
+
+
+
+
+
+ {(cart?.lines?.nodes ?? []).map((line) => (
+
+ ))}
+
+
+ {cartHasItems &&
}
+
+
+ )
+}
+
+function CartEmpty({ hidden = false }: { hidden: boolean; layout?: CartMainProps['layout'] }) {
+ const { close } = useAside()
+ return (
+
+
+
Looks like you haven’t added anything yet, let’s get you started!
+
+
+ Continue shopping →
+
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartSummary.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartSummary.tsx
new file mode 100644
index 000000000..2776c0a3f
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/CartSummary.tsx
@@ -0,0 +1,81 @@
+import type { CartApiQueryFragment } from 'storefrontapi.generated'
+import type { CartLayout } from '~/components/CartMain'
+import { CartForm, Money, type OptimisticCart } from '@shopify/hydrogen'
+
+type CartSummaryProps = {
+ cart: OptimisticCart
+ layout: CartLayout
+}
+
+export function CartSummary({ cart, layout }: CartSummaryProps) {
+ const className = layout === 'page' ? 'cart-summary-page' : 'cart-summary-aside'
+
+ return (
+
+
Totals
+
+ Subtotal
+ {cart.cost?.subtotalAmount?.amount ? : '-'}
+
+
+
+
+ )
+}
+function CartCheckoutActions({ checkoutUrl }: { checkoutUrl?: string }) {
+ if (!checkoutUrl) return null
+
+ return (
+
+ )
+}
+
+function CartDiscounts({ discountCodes }: { discountCodes?: CartApiQueryFragment['discountCodes'] }) {
+ const codes: string[] = discountCodes?.filter((discount) => discount.applicable)?.map(({ code }) => code) || []
+
+ return (
+
+ {/* Have existing discount, display it with a remove option */}
+
+
+
Discount(s)
+
+
+ {codes?.join(', ')}
+
+ Remove
+
+
+
+
+
+ {/* Show an input to apply a discount */}
+
+
+
+
+ Apply
+
+
+
+ )
+}
+
+function UpdateDiscountForm({ discountCodes, children }: { discountCodes?: string[]; children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/Footer.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/Footer.tsx
new file mode 100644
index 000000000..3939c83cc
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/Footer.tsx
@@ -0,0 +1,113 @@
+import { Suspense } from 'react'
+import { Await, NavLink } from '@remix-run/react'
+import type { FooterQuery, HeaderQuery } from 'storefrontapi.generated'
+
+interface FooterProps {
+ footer: Promise
+ header: HeaderQuery
+ publicStoreDomain: string
+}
+
+export function Footer({ footer: footerPromise, header, publicStoreDomain }: FooterProps) {
+ return (
+
+
+ {(footer) => (
+
+ {footer?.menu && header.shop.primaryDomain?.url && (
+
+ )}
+
+ )}
+
+
+ )
+}
+
+function FooterMenu({
+ menu,
+ primaryDomainUrl,
+ publicStoreDomain,
+}: {
+ menu: FooterQuery['menu']
+ primaryDomainUrl: FooterProps['header']['shop']['primaryDomain']['url']
+ publicStoreDomain: string
+}) {
+ return (
+
+ {(menu || FALLBACK_FOOTER_MENU).items.map((item) => {
+ if (!item.url) return null
+ // if the url is internal, we strip the domain
+ const url =
+ item.url.includes('myshopify.com') ||
+ item.url.includes(publicStoreDomain) ||
+ item.url.includes(primaryDomainUrl)
+ ? new URL(item.url).pathname
+ : item.url
+ const isExternal = !url.startsWith('/')
+ return isExternal ? (
+
+ {item.title}
+
+ ) : (
+
+ {item.title}
+
+ )
+ })}
+
+ )
+}
+
+const FALLBACK_FOOTER_MENU = {
+ id: 'gid://shopify/Menu/199655620664',
+ items: [
+ {
+ id: 'gid://shopify/MenuItem/461633060920',
+ resourceId: 'gid://shopify/ShopPolicy/23358046264',
+ tags: [],
+ title: 'Privacy Policy',
+ type: 'SHOP_POLICY',
+ url: '/policies/privacy-policy',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461633093688',
+ resourceId: 'gid://shopify/ShopPolicy/23358013496',
+ tags: [],
+ title: 'Refund Policy',
+ type: 'SHOP_POLICY',
+ url: '/policies/refund-policy',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461633126456',
+ resourceId: 'gid://shopify/ShopPolicy/23358111800',
+ tags: [],
+ title: 'Shipping Policy',
+ type: 'SHOP_POLICY',
+ url: '/policies/shipping-policy',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461633159224',
+ resourceId: 'gid://shopify/ShopPolicy/23358079032',
+ tags: [],
+ title: 'Terms of Service',
+ type: 'SHOP_POLICY',
+ url: '/policies/terms-of-service',
+ items: [],
+ },
+ ],
+}
+
+function activeLinkStyle({ isActive, isPending }: { isActive: boolean; isPending: boolean }) {
+ return {
+ fontWeight: isActive ? 'bold' : undefined,
+ color: isPending ? 'grey' : 'white',
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/Header.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/Header.tsx
new file mode 100644
index 000000000..5d714c27f
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/Header.tsx
@@ -0,0 +1,193 @@
+import { Suspense } from 'react'
+import { Await, NavLink } from '@remix-run/react'
+import { type CartViewPayload, useAnalytics } from '@shopify/hydrogen'
+import type { HeaderQuery, CartApiQueryFragment } from 'storefrontapi.generated'
+import { useAside } from '~/components/Aside'
+
+interface HeaderProps {
+ header: HeaderQuery
+ cart: Promise
+ publicStoreDomain: string
+}
+
+type Viewport = 'desktop' | 'mobile'
+
+export function Header({ header, cart, publicStoreDomain }: HeaderProps) {
+ const { shop, menu } = header
+ return (
+
+ )
+}
+
+export function HeaderMenu({
+ menu,
+ primaryDomainUrl,
+ viewport,
+ publicStoreDomain,
+}: {
+ menu: HeaderProps['header']['menu']
+ primaryDomainUrl: HeaderProps['header']['shop']['primaryDomain']['url']
+ viewport: Viewport
+ publicStoreDomain: HeaderProps['publicStoreDomain']
+}) {
+ const className = `header-menu-${viewport}`
+ const { close } = useAside()
+
+ return (
+
+ {viewport === 'mobile' && (
+
+ Home
+
+ )}
+ {(menu || FALLBACK_HEADER_MENU).items.map((item) => {
+ if (!item.url) return null
+
+ // if the url is internal, we strip the domain
+ const url =
+ item.url.includes('myshopify.com') ||
+ item.url.includes(publicStoreDomain) ||
+ item.url.includes(primaryDomainUrl)
+ ? new URL(item.url).pathname
+ : item.url
+ return (
+
+ {item.title}
+
+ )
+ })}
+
+ )
+}
+
+function HeaderCtas({ cart }: Pick) {
+ return (
+
+
+
+
+
+ )
+}
+
+function HeaderMenuMobileToggle() {
+ const { open } = useAside()
+ return (
+ open('mobile')}>
+ ☰
+
+ )
+}
+
+function SearchToggle() {
+ const { open } = useAside()
+ return (
+ open('search')}>
+ Search
+
+ )
+}
+
+function CartBadge({ count }: { count: number | null }) {
+ const { open } = useAside()
+ const { publish, shop, cart, prevCart } = useAnalytics()
+
+ return (
+ {
+ e.preventDefault()
+ open('cart')
+ publish('cart_viewed', {
+ cart,
+ prevCart,
+ shop,
+ url: window.location.href || '',
+ } as CartViewPayload)
+ }}
+ >
+ Cart {count === null ? : count}
+
+ )
+}
+
+function CartToggle({ cart }: Pick) {
+ return (
+ }>
+
+ {(cart) => {
+ if (!cart) return
+ return
+ }}
+
+
+ )
+}
+
+const FALLBACK_HEADER_MENU = {
+ id: 'gid://shopify/Menu/199655587896',
+ items: [
+ {
+ id: 'gid://shopify/MenuItem/461609500728',
+ resourceId: null,
+ tags: [],
+ title: 'Collections',
+ type: 'HTTP',
+ url: '/collections',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461609533496',
+ resourceId: null,
+ tags: [],
+ title: 'Blog',
+ type: 'HTTP',
+ url: '/blogs/journal',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461609566264',
+ resourceId: null,
+ tags: [],
+ title: 'Policies',
+ type: 'HTTP',
+ url: '/policies',
+ items: [],
+ },
+ {
+ id: 'gid://shopify/MenuItem/461609599032',
+ resourceId: 'gid://shopify/Page/92591030328',
+ tags: [],
+ title: 'About',
+ type: 'PAGE',
+ url: '/pages/about',
+ items: [],
+ },
+ ],
+}
+
+function activeLinkStyle({ isActive, isPending }: { isActive: boolean; isPending: boolean }) {
+ return {
+ fontWeight: isActive ? 'bold' : undefined,
+ color: isPending ? 'grey' : 'black',
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/PageLayout.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/PageLayout.tsx
new file mode 100644
index 000000000..98dd86d1e
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/PageLayout.tsx
@@ -0,0 +1,124 @@
+import { Await, Link } from '@remix-run/react'
+import { Suspense } from 'react'
+import type { CartApiQueryFragment, FooterQuery, HeaderQuery } from 'storefrontapi.generated'
+import { Aside } from '~/components/Aside'
+import { Footer } from '~/components/Footer'
+import { Header, HeaderMenu } from '~/components/Header'
+import { CartMain } from '~/components/CartMain'
+import { SEARCH_ENDPOINT, SearchFormPredictive } from '~/components/SearchFormPredictive'
+import { SearchResultsPredictive } from '~/components/SearchResultsPredictive'
+
+interface PageLayoutProps {
+ cart: Promise
+ footer: Promise
+ header: HeaderQuery
+ publicStoreDomain: string
+ children?: React.ReactNode
+}
+
+export function PageLayout({ cart, children = null, footer, header, publicStoreDomain }: PageLayoutProps) {
+ return (
+
+
+
+
+ {header && }
+ {children}
+
+
+ )
+}
+
+function CartAside({ cart }: { cart: PageLayoutProps['cart'] }) {
+ return (
+
+ Loading cart ...}>
+
+ {(cart) => {
+ return
+ }}
+
+
+
+ )
+}
+
+function SearchAside() {
+ return (
+
+
+
+
+ {({ fetchResults, goToSearch, inputRef }) => (
+ <>
+
+
+ Search
+ >
+ )}
+
+
+
+ {({ items, total, term, state, inputRef, closeSearch }) => {
+ const { articles, collections, pages, products, queries } = items
+
+ if (state === 'loading' && term.current) {
+ return Loading...
+ }
+
+ if (!total) {
+ return
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {term.current && total ? (
+
+
+ View all results for {term.current}
+ →
+
+
+ ) : null}
+ >
+ )
+ }}
+
+
+
+ )
+}
+
+function MobileMenuAside({
+ header,
+ publicStoreDomain,
+}: {
+ header: PageLayoutProps['header']
+ publicStoreDomain: PageLayoutProps['publicStoreDomain']
+}) {
+ return (
+ header.menu &&
+ header.shop.primaryDomain?.url && (
+
+ )
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/PaginatedResourceSection.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/PaginatedResourceSection.tsx
new file mode 100644
index 000000000..90c3db885
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/PaginatedResourceSection.tsx
@@ -0,0 +1,32 @@
+import * as React from 'react'
+import { Pagination } from '@shopify/hydrogen'
+
+/**
+ * is a component that encapsulate how the previous and next behaviors throughout your application.
+ */
+
+export function PaginatedResourceSection({
+ connection,
+ children,
+ resourcesClassName,
+}: {
+ connection: React.ComponentProps>['connection']
+ children: React.FunctionComponent<{ node: NodesType; index: number }>
+ resourcesClassName?: string
+}) {
+ return (
+
+ {({ nodes, isLoading, PreviousLink, NextLink }) => {
+ const resoucesMarkup = nodes.map((node, index) => children({ node, index }))
+
+ return (
+
+
{isLoading ? 'Loading...' : ↑ Load previous }
+ {resourcesClassName ?
{resoucesMarkup}
: resoucesMarkup}
+
{isLoading ? 'Loading...' : Load more ↓ }
+
+ )
+ }}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductForm.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductForm.tsx
new file mode 100644
index 000000000..8b74f4db3
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductForm.tsx
@@ -0,0 +1,77 @@
+import { Link } from '@remix-run/react'
+import { type VariantOption, VariantSelector } from '@shopify/hydrogen'
+import type { ProductFragment, ProductVariantFragment } from 'storefrontapi.generated'
+import { AddToCartButton } from '~/components/AddToCartButton'
+import { useAside } from '~/components/Aside'
+
+export function ProductForm({
+ product,
+ selectedVariant,
+ variants,
+}: {
+ product: ProductFragment
+ selectedVariant: ProductFragment['selectedVariant']
+ variants: Array
+}) {
+ const { open } = useAside()
+ return (
+
+
option.values.length > 1)}
+ variants={variants}
+ >
+ {({ option }) => }
+
+
+
{
+ open('cart')
+ }}
+ lines={
+ selectedVariant
+ ? [
+ {
+ merchandiseId: selectedVariant.id,
+ quantity: 1,
+ selectedVariant,
+ },
+ ]
+ : []
+ }
+ >
+ {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
+
+
+ )
+}
+
+function ProductOptions({ option }: { option: VariantOption }) {
+ return (
+
+
{option.name}
+
+ {option.values.map(({ value, isAvailable, isActive, to }) => {
+ return (
+
+ {value}
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductImage.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductImage.tsx
new file mode 100644
index 000000000..4cd55b064
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductImage.tsx
@@ -0,0 +1,19 @@
+import type { ProductVariantFragment } from 'storefrontapi.generated'
+import { Image } from '@shopify/hydrogen'
+
+export function ProductImage({ image }: { image: ProductVariantFragment['image'] }) {
+ if (!image) {
+ return
+ }
+ return (
+
+
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductPrice.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductPrice.tsx
new file mode 100644
index 000000000..4bbd3c20a
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/ProductPrice.tsx
@@ -0,0 +1,21 @@
+import { Money } from '@shopify/hydrogen'
+import type { MoneyV2 } from '@shopify/hydrogen/storefront-api-types'
+
+export function ProductPrice({ price, compareAtPrice }: { price?: MoneyV2; compareAtPrice?: MoneyV2 | null }) {
+ return (
+
+ {compareAtPrice ? (
+
+ {price ? : null}
+
+
+
+
+ ) : price ? (
+
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchForm.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchForm.tsx
new file mode 100644
index 000000000..8f08ce33f
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchForm.tsx
@@ -0,0 +1,66 @@
+import { useRef, useEffect } from 'react'
+import { Form, type FormProps } from '@remix-run/react'
+
+type SearchFormProps = Omit & {
+ children: (args: { inputRef: React.RefObject }) => React.ReactNode
+}
+
+/**
+ * Search form component that sends search requests to the `/search` route.
+ * @example
+ * ```tsx
+ *
+ * {({inputRef}) => (
+ * <>
+ *
+ * Search
+ * >
+ * )}
+ *
+ */
+export function SearchForm({ children, ...props }: SearchFormProps) {
+ const inputRef = useRef(null)
+
+ useFocusOnCmdK(inputRef)
+
+ if (typeof children !== 'function') {
+ return null
+ }
+
+ return (
+
+ )
+}
+
+/**
+ * Focuses the input when cmd+k is pressed
+ */
+function useFocusOnCmdK(inputRef: React.RefObject) {
+ // focus the input when cmd+k is pressed
+ useEffect(() => {
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === 'k' && event.metaKey) {
+ event.preventDefault()
+ inputRef.current?.focus()
+ }
+
+ if (event.key === 'Escape') {
+ inputRef.current?.blur()
+ }
+ }
+
+ document.addEventListener('keydown', handleKeyDown)
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [inputRef])
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchFormPredictive.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchFormPredictive.tsx
new file mode 100644
index 000000000..a8831fd67
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchFormPredictive.tsx
@@ -0,0 +1,71 @@
+import { useFetcher, useNavigate, type FormProps, type Fetcher } from '@remix-run/react'
+import React, { useRef, useEffect } from 'react'
+import type { PredictiveSearchReturn } from '~/lib/search'
+import { useAside } from './Aside'
+
+type SearchFormPredictiveChildren = (args: {
+ fetchResults: (event: React.ChangeEvent) => void
+ goToSearch: () => void
+ inputRef: React.MutableRefObject
+ fetcher: Fetcher
+}) => React.ReactNode
+
+type SearchFormPredictiveProps = Omit & {
+ children: SearchFormPredictiveChildren | null
+}
+
+export const SEARCH_ENDPOINT = '/search'
+
+/**
+ * Search form component that sends search requests to the `/search` route
+ **/
+export function SearchFormPredictive({
+ children,
+ className = 'predictive-search-form',
+ ...props
+}: SearchFormPredictiveProps) {
+ const fetcher = useFetcher({ key: 'search' })
+ const inputRef = useRef(null)
+ const navigate = useNavigate()
+ const aside = useAside()
+
+ /** Reset the input value and blur the input */
+ function resetInput(event: React.FormEvent) {
+ event.preventDefault()
+ event.stopPropagation()
+ if (inputRef?.current?.value) {
+ inputRef.current.blur()
+ }
+ }
+
+ /** Navigate to the search page with the current input value */
+ function goToSearch() {
+ const term = inputRef?.current?.value
+ navigate(SEARCH_ENDPOINT + (term ? `?q=${term}` : ''))
+ aside.close()
+ }
+
+ /** Fetch search results based on the input value */
+ function fetchResults(event: React.ChangeEvent) {
+ fetcher.submit(
+ { q: event.target.value || '', limit: 5, predictive: true },
+ { method: 'GET', action: SEARCH_ENDPOINT },
+ )
+ }
+
+ // ensure the passed input has a type of search, because SearchResults
+ // will select the element based on the input
+ useEffect(() => {
+ inputRef?.current?.setAttribute('type', 'search')
+ }, [])
+
+ if (typeof children !== 'function') {
+ return null
+ }
+
+ return (
+
+ {children({ inputRef, fetcher, fetchResults, goToSearch })}
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchResults.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchResults.tsx
new file mode 100644
index 000000000..d6714348c
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchResults.tsx
@@ -0,0 +1,143 @@
+import { Link } from '@remix-run/react'
+import { Image, Money, Pagination } from '@shopify/hydrogen'
+import { urlWithTrackingParams, type RegularSearchReturn } from '~/lib/search'
+
+type SearchItems = RegularSearchReturn['result']['items']
+type PartialSearchResult = Pick &
+ Pick
+
+type SearchResultsProps = RegularSearchReturn & {
+ children: (args: SearchItems & { term: string }) => React.ReactNode
+}
+
+export function SearchResults({ term, result, children }: Omit) {
+ if (!result?.total) {
+ return null
+ }
+
+ return children({ ...result.items, term })
+}
+
+SearchResults.Articles = SearchResultsArticles
+SearchResults.Pages = SearchResultsPages
+SearchResults.Products = SearchResultsProducts
+SearchResults.Empty = SearchResultsEmpty
+
+function SearchResultsArticles({ term, articles }: PartialSearchResult<'articles'>) {
+ if (!articles?.nodes.length) {
+ return null
+ }
+
+ return (
+
+
Articles
+
+ {articles?.nodes?.map((article) => {
+ const articleUrl = urlWithTrackingParams({
+ baseUrl: `/blogs/${article.handle}`,
+ trackingParams: article.trackingParameters,
+ term,
+ })
+
+ return (
+
+
+ {article.title}
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+function SearchResultsPages({ term, pages }: PartialSearchResult<'pages'>) {
+ if (!pages?.nodes.length) {
+ return null
+ }
+
+ return (
+
+
Pages
+
+ {pages?.nodes?.map((page) => {
+ const pageUrl = urlWithTrackingParams({
+ baseUrl: `/pages/${page.handle}`,
+ trackingParams: page.trackingParameters,
+ term,
+ })
+
+ return (
+
+
+ {page.title}
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+function SearchResultsProducts({ term, products }: PartialSearchResult<'products'>) {
+ if (!products?.nodes.length) {
+ return null
+ }
+
+ return (
+
+
Products
+
+ {({ nodes, isLoading, NextLink, PreviousLink }) => {
+ const ItemsMarkup = nodes.map((product) => {
+ const productUrl = urlWithTrackingParams({
+ baseUrl: `/products/${product.handle}`,
+ trackingParams: product.trackingParameters,
+ term,
+ })
+
+ return (
+
+
+ {product.variants.nodes[0].image && (
+
+ )}
+
+
{product.title}
+
+
+
+
+
+
+ )
+ })
+
+ return (
+
+
+
{isLoading ? 'Loading...' : ↑ Load previous }
+
+
+ {ItemsMarkup}
+
+
+
+ {isLoading ? 'Loading...' : Load more ↓ }
+
+
+ )
+ }}
+
+
+
+ )
+}
+
+function SearchResultsEmpty() {
+ return No results, try a different search.
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchResultsPredictive.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchResultsPredictive.tsx
new file mode 100644
index 000000000..bf3aa9bc8
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/components/SearchResultsPredictive.tsx
@@ -0,0 +1,273 @@
+import { Link, useFetcher, type Fetcher } from '@remix-run/react'
+import { Image, Money } from '@shopify/hydrogen'
+import React, { useRef, useEffect } from 'react'
+import { getEmptyPredictiveSearchResult, urlWithTrackingParams, type PredictiveSearchReturn } from '~/lib/search'
+import { useAside } from './Aside'
+
+type PredictiveSearchItems = PredictiveSearchReturn['result']['items']
+
+type UsePredictiveSearchReturn = {
+ term: React.MutableRefObject
+ total: number
+ inputRef: React.MutableRefObject
+ items: PredictiveSearchItems
+ fetcher: Fetcher
+}
+
+type SearchResultsPredictiveArgs = Pick & {
+ state: Fetcher['state']
+ closeSearch: () => void
+}
+
+type PartialPredictiveSearchResult<
+ ItemType extends keyof PredictiveSearchItems,
+ ExtraProps extends keyof SearchResultsPredictiveArgs = 'term' | 'closeSearch',
+> = Pick & Pick
+
+type SearchResultsPredictiveProps = {
+ children: (args: SearchResultsPredictiveArgs) => React.ReactNode
+}
+
+/**
+ * Component that renders predictive search results
+ */
+export function SearchResultsPredictive({ children }: SearchResultsPredictiveProps) {
+ const aside = useAside()
+ const { term, inputRef, fetcher, total, items } = usePredictiveSearch()
+
+ /*
+ * Utility that resets the search input
+ */
+ function resetInput() {
+ if (inputRef.current) {
+ inputRef.current.blur()
+ inputRef.current.value = ''
+ }
+ }
+
+ /**
+ * Utility that resets the search input and closes the search aside
+ */
+ function closeSearch() {
+ resetInput()
+ aside.close()
+ }
+
+ return children({
+ items,
+ closeSearch,
+ inputRef,
+ state: fetcher.state,
+ term,
+ total,
+ })
+}
+
+SearchResultsPredictive.Articles = SearchResultsPredictiveArticles
+SearchResultsPredictive.Collections = SearchResultsPredictiveCollections
+SearchResultsPredictive.Pages = SearchResultsPredictivePages
+SearchResultsPredictive.Products = SearchResultsPredictiveProducts
+SearchResultsPredictive.Queries = SearchResultsPredictiveQueries
+SearchResultsPredictive.Empty = SearchResultsPredictiveEmpty
+
+function SearchResultsPredictiveArticles({ term, articles, closeSearch }: PartialPredictiveSearchResult<'articles'>) {
+ if (!articles.length) return null
+
+ return (
+
+
Articles
+
+ {articles.map((article) => {
+ const articleUrl = urlWithTrackingParams({
+ baseUrl: `/blogs/${article.blog.handle}/${article.handle}`,
+ trackingParams: article.trackingParameters,
+ term: term.current ?? '',
+ })
+
+ return (
+
+
+ {article.image?.url && (
+
+ )}
+
+ {article.title}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function SearchResultsPredictiveCollections({
+ term,
+ collections,
+ closeSearch,
+}: PartialPredictiveSearchResult<'collections'>) {
+ if (!collections.length) return null
+
+ return (
+
+
Collections
+
+ {collections.map((collection) => {
+ const colllectionUrl = urlWithTrackingParams({
+ baseUrl: `/collections/${collection.handle}`,
+ trackingParams: collection.trackingParameters,
+ term: term.current,
+ })
+
+ return (
+
+
+ {collection.image?.url && (
+
+ )}
+
+ {collection.title}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function SearchResultsPredictivePages({ term, pages, closeSearch }: PartialPredictiveSearchResult<'pages'>) {
+ if (!pages.length) return null
+
+ return (
+
+
Pages
+
+ {pages.map((page) => {
+ const pageUrl = urlWithTrackingParams({
+ baseUrl: `/pages/${page.handle}`,
+ trackingParams: page.trackingParameters,
+ term: term.current,
+ })
+
+ return (
+
+
+
+ {page.title}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function SearchResultsPredictiveProducts({ term, products, closeSearch }: PartialPredictiveSearchResult<'products'>) {
+ if (!products.length) return null
+
+ return (
+
+
Products
+
+ {products.map((product) => {
+ const productUrl = urlWithTrackingParams({
+ baseUrl: `/products/${product.handle}`,
+ trackingParams: product.trackingParameters,
+ term: term.current,
+ })
+
+ const image = product?.variants?.nodes?.[0].image
+ return (
+
+
+ {image && }
+
+
{product.title}
+
+ {product?.variants?.nodes?.[0].price && }
+
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function SearchResultsPredictiveQueries({ queries, inputRef }: PartialPredictiveSearchResult<'queries', 'inputRef'>) {
+ if (!queries.length) return null
+
+ return (
+
+ )
+}
+
+const FEATURED_COLLECTION_QUERY = `#graphql
+ fragment FeaturedCollection on Collection {
+ id
+ title
+ image {
+ id
+ url
+ altText
+ width
+ height
+ }
+ handle
+ }
+ query FeaturedCollection($country: CountryCode, $language: LanguageCode)
+ @inContext(country: $country, language: $language) {
+ collections(first: 1, sortKey: UPDATED_AT, reverse: true) {
+ nodes {
+ ...FeaturedCollection
+ }
+ }
+ }
+` as const
+
+const RECOMMENDED_PRODUCTS_QUERY = `#graphql
+ fragment RecommendedProduct on Product {
+ id
+ title
+ handle
+ priceRange {
+ minVariantPrice {
+ amount
+ currencyCode
+ }
+ }
+ images(first: 1) {
+ nodes {
+ id
+ url
+ altText
+ width
+ height
+ }
+ }
+ }
+ query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
+ @inContext(country: $country, language: $language) {
+ products(first: 4, sortKey: UPDATED_AT, reverse: true) {
+ nodes {
+ ...RecommendedProduct
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs.$blogHandle.$articleHandle.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs.$blogHandle.$articleHandle.tsx
new file mode 100644
index 000000000..d1b100e09
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs.$blogHandle.$articleHandle.tsx
@@ -0,0 +1,110 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, type MetaFunction } from '@remix-run/react'
+import { Image } from '@shopify/hydrogen'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.article.title ?? ''} article` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, params }: LoaderFunctionArgs) {
+ const { blogHandle, articleHandle } = params
+
+ if (!articleHandle || !blogHandle) {
+ throw new Response('Not found', { status: 404 })
+ }
+
+ const [{ blog }] = await Promise.all([
+ context.storefront.query(ARTICLE_QUERY, {
+ variables: { blogHandle, articleHandle },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ if (!blog?.articleByHandle) {
+ throw new Response(null, { status: 404 })
+ }
+
+ const article = blog.articleByHandle
+
+ return { article }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Article() {
+ const { article } = useLoaderData()
+ const { title, image, contentHtml, author } = article
+
+ const publishedDate = new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(article.publishedAt))
+
+ return (
+
+
+ {title}
+
+ {publishedDate} · {author?.name}
+
+
+
+ {image &&
}
+
+
+ )
+}
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog#field-blog-articlebyhandle
+const ARTICLE_QUERY = `#graphql
+ query Article(
+ $articleHandle: String!
+ $blogHandle: String!
+ $country: CountryCode
+ $language: LanguageCode
+ ) @inContext(language: $language, country: $country) {
+ blog(handle: $blogHandle) {
+ articleByHandle(handle: $articleHandle) {
+ title
+ contentHtml
+ publishedAt
+ author: authorV2 {
+ name
+ }
+ image {
+ id
+ altText
+ url
+ width
+ height
+ }
+ seo {
+ description
+ title
+ }
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs.$blogHandle._index.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs.$blogHandle._index.tsx
new file mode 100644
index 000000000..f5b3c00b5
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs.$blogHandle._index.tsx
@@ -0,0 +1,161 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { Link, useLoaderData, type MetaFunction } from '@remix-run/react'
+import { Image, getPaginationVariables } from '@shopify/hydrogen'
+import type { ArticleItemFragment } from 'storefrontapi.generated'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.blog.title ?? ''} blog` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, request, params }: LoaderFunctionArgs) {
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 4,
+ })
+
+ if (!params.blogHandle) {
+ throw new Response(`blog not found`, { status: 404 })
+ }
+
+ const [{ blog }] = await Promise.all([
+ context.storefront.query(BLOGS_QUERY, {
+ variables: {
+ blogHandle: params.blogHandle,
+ ...paginationVariables,
+ },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ if (!blog?.articles) {
+ throw new Response('Not found', { status: 404 })
+ }
+
+ return { blog }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Blog() {
+ const { blog } = useLoaderData()
+ const { articles } = blog
+
+ return (
+
+
{blog.title}
+
+
+ {({ node: article, index }) => (
+
+ )}
+
+
+
+ )
+}
+
+function ArticleItem({ article, loading }: { article: ArticleItemFragment; loading?: HTMLImageElement['loading'] }) {
+ const publishedAt = new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(article.publishedAt!))
+ return (
+
+
+ {article.image && (
+
+
+
+ )}
+
{article.title}
+
{publishedAt}
+
+
+ )
+}
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog
+const BLOGS_QUERY = `#graphql
+ query Blog(
+ $language: LanguageCode
+ $blogHandle: String!
+ $first: Int
+ $last: Int
+ $startCursor: String
+ $endCursor: String
+ ) @inContext(language: $language) {
+ blog(handle: $blogHandle) {
+ title
+ seo {
+ title
+ description
+ }
+ articles(
+ first: $first,
+ last: $last,
+ before: $startCursor,
+ after: $endCursor
+ ) {
+ nodes {
+ ...ArticleItem
+ }
+ pageInfo {
+ hasPreviousPage
+ hasNextPage
+ hasNextPage
+ endCursor
+ startCursor
+ }
+
+ }
+ }
+ }
+ fragment ArticleItem on Article {
+ author: authorV2 {
+ name
+ }
+ contentHtml
+ handle
+ id
+ image {
+ id
+ altText
+ url
+ width
+ height
+ }
+ publishedAt
+ title
+ blog {
+ handle
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs._index.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs._index.tsx
new file mode 100644
index 000000000..87b8388a1
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/blogs._index.tsx
@@ -0,0 +1,101 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { Link, useLoaderData, type MetaFunction } from '@remix-run/react'
+import { getPaginationVariables } from '@shopify/hydrogen'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export const meta: MetaFunction = () => {
+ return [{ title: `Hydrogen | Blogs` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, request }: LoaderFunctionArgs) {
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 10,
+ })
+
+ const [{ blogs }] = await Promise.all([
+ context.storefront.query(BLOGS_QUERY, {
+ variables: {
+ ...paginationVariables,
+ },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ return { blogs }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Blogs() {
+ const { blogs } = useLoaderData()
+
+ return (
+
+
Blogs
+
+
+ {({ node: blog }) => (
+
+ {blog.title}
+
+ )}
+
+
+
+ )
+}
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog
+const BLOGS_QUERY = `#graphql
+ query Blogs(
+ $country: CountryCode
+ $endCursor: String
+ $first: Int
+ $language: LanguageCode
+ $last: Int
+ $startCursor: String
+ ) @inContext(country: $country, language: $language) {
+ blogs(
+ first: $first,
+ last: $last,
+ before: $startCursor,
+ after: $endCursor
+ ) {
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
+ nodes {
+ title
+ handle
+ seo {
+ title
+ description
+ }
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/cart.$lines.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/cart.$lines.tsx
new file mode 100644
index 000000000..45e267950
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/cart.$lines.tsx
@@ -0,0 +1,69 @@
+import { redirect, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+
+/**
+ * Automatically creates a new cart based on the URL and redirects straight to checkout.
+ * Expected URL structure:
+ * ```js
+ * /cart/:
+ *
+ * ```
+ *
+ * More than one `:` separated by a comma, can be supplied in the URL, for
+ * carts with more than one product variant.
+ *
+ * @example
+ * Example path creating a cart with two product variants, different quantities, and a discount code in the querystring:
+ * ```js
+ * /cart/41007289663544:1,41007289696312:2?discount=HYDROBOARD
+ *
+ * ```
+ */
+export async function loader({ request, context, params }: LoaderFunctionArgs) {
+ const { cart } = context
+ const { lines } = params
+ if (!lines) return redirect('/cart')
+ const linesMap = lines.split(',').map((line) => {
+ const lineDetails = line.split(':')
+ const variantId = lineDetails[0]
+ const quantity = parseInt(lineDetails[1], 10)
+
+ return {
+ merchandiseId: `gid://shopify/ProductVariant/${variantId}`,
+ quantity,
+ }
+ })
+
+ const url = new URL(request.url)
+ const searchParams = new URLSearchParams(url.search)
+
+ const discount = searchParams.get('discount')
+ const discountArray = discount ? [discount] : []
+
+ // create a cart
+ const result = await cart.create({
+ lines: linesMap,
+ discountCodes: discountArray,
+ })
+
+ const cartResult = result.cart
+
+ if (result.errors?.length || !cartResult) {
+ throw new Response('Link may be expired. Try checking the URL.', {
+ status: 410,
+ })
+ }
+
+ // Update cart id in cookie
+ const headers = cart.setCartId(cartResult.id)
+
+ // redirect to checkout
+ if (cartResult.checkoutUrl) {
+ return redirect(cartResult.checkoutUrl, { headers })
+ } else {
+ throw new Error('No checkout URL found')
+ }
+}
+
+export default function Component() {
+ return null
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/cart.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/cart.tsx
new file mode 100644
index 000000000..16f70173c
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/cart.tsx
@@ -0,0 +1,97 @@
+import { Await, type MetaFunction, useRouteLoaderData } from '@remix-run/react'
+import { Suspense } from 'react'
+import type { CartQueryDataReturn } from '@shopify/hydrogen'
+import { CartForm } from '@shopify/hydrogen'
+import { json, type ActionFunctionArgs } from '@netlify/remix-runtime'
+import { CartMain } from '~/components/CartMain'
+import type { RootLoader } from '~/root'
+
+export const meta: MetaFunction = () => {
+ return [{ title: `Hydrogen | Cart` }]
+}
+
+export async function action({ request, context }: ActionFunctionArgs) {
+ const { cart } = context
+
+ const formData = await request.formData()
+
+ const { action, inputs } = CartForm.getFormInput(formData)
+
+ if (!action) {
+ throw new Error('No action provided')
+ }
+
+ let status = 200
+ let result: CartQueryDataReturn
+
+ switch (action) {
+ case CartForm.ACTIONS.LinesAdd:
+ result = await cart.addLines(inputs.lines)
+ break
+ case CartForm.ACTIONS.LinesUpdate:
+ result = await cart.updateLines(inputs.lines)
+ break
+ case CartForm.ACTIONS.LinesRemove:
+ result = await cart.removeLines(inputs.lineIds)
+ break
+ case CartForm.ACTIONS.DiscountCodesUpdate: {
+ const formDiscountCode = inputs.discountCode
+
+ // User inputted discount code
+ const discountCodes = (formDiscountCode ? [formDiscountCode] : []) as string[]
+
+ // Combine discount codes already applied on cart
+ discountCodes.push(...inputs.discountCodes)
+
+ result = await cart.updateDiscountCodes(discountCodes)
+ break
+ }
+ case CartForm.ACTIONS.BuyerIdentityUpdate: {
+ result = await cart.updateBuyerIdentity({
+ ...inputs.buyerIdentity,
+ })
+ break
+ }
+ default:
+ throw new Error(`${action} cart action is not defined`)
+ }
+
+ const cartId = result?.cart?.id
+ const headers = cartId ? cart.setCartId(result.cart.id) : new Headers()
+ const { cart: cartResult, errors } = result
+
+ const redirectTo = formData.get('redirectTo') ?? null
+ if (typeof redirectTo === 'string') {
+ status = 303
+ headers.set('Location', redirectTo)
+ }
+
+ return json(
+ {
+ cart: cartResult,
+ errors,
+ analytics: {
+ cartId,
+ },
+ },
+ { status, headers },
+ )
+}
+
+export default function Cart() {
+ const rootData = useRouteLoaderData('root')
+ if (!rootData) return null
+
+ return (
+
+
Cart
+
Loading cart ...}>
+ An error occurred }>
+ {(cart) => {
+ return
+ }}
+
+
+
+ )
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections.$handle.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections.$handle.tsx
new file mode 100644
index 000000000..dbef15100
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections.$handle.tsx
@@ -0,0 +1,180 @@
+import { defer, redirect, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, Link, type MetaFunction } from '@remix-run/react'
+import { getPaginationVariables, Image, Money, Analytics } from '@shopify/hydrogen'
+import type { ProductItemFragment } from 'storefrontapi.generated'
+import { useVariantUrl } from '~/lib/variants'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.collection.title ?? ''} Collection` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, params, request }: LoaderFunctionArgs) {
+ const { handle } = params
+ const { storefront } = context
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 8,
+ })
+
+ if (!handle) {
+ throw redirect('/collections')
+ }
+
+ const [{ collection }] = await Promise.all([
+ storefront.query(COLLECTION_QUERY, {
+ variables: { handle, ...paginationVariables },
+ // Add other queries here, so that they are loaded in parallel
+ }),
+ ])
+
+ if (!collection) {
+ throw new Response(`Collection ${handle} not found`, {
+ status: 404,
+ })
+ }
+
+ return {
+ collection,
+ }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Collection() {
+ const { collection } = useLoaderData()
+
+ return (
+
+
{collection.title}
+
{collection.description}
+
+ {({ node: product, index }) => (
+
+ )}
+
+
+
+ )
+}
+
+function ProductItem({ product, loading }: { product: ProductItemFragment; loading?: 'eager' | 'lazy' }) {
+ const variant = product.variants.nodes[0]
+ const variantUrl = useVariantUrl(product.handle, variant.selectedOptions)
+ return (
+
+ {product.featuredImage && (
+
+ )}
+ {product.title}
+
+
+
+
+ )
+}
+
+const PRODUCT_ITEM_FRAGMENT = `#graphql
+ fragment MoneyProductItem on MoneyV2 {
+ amount
+ currencyCode
+ }
+ fragment ProductItem on Product {
+ id
+ handle
+ title
+ featuredImage {
+ id
+ altText
+ url
+ width
+ height
+ }
+ priceRange {
+ minVariantPrice {
+ ...MoneyProductItem
+ }
+ maxVariantPrice {
+ ...MoneyProductItem
+ }
+ }
+ variants(first: 1) {
+ nodes {
+ selectedOptions {
+ name
+ value
+ }
+ }
+ }
+ }
+` as const
+
+// NOTE: https://shopify.dev/docs/api/storefront/2022-04/objects/collection
+const COLLECTION_QUERY = `#graphql
+ ${PRODUCT_ITEM_FRAGMENT}
+ query Collection(
+ $handle: String!
+ $country: CountryCode
+ $language: LanguageCode
+ $first: Int
+ $last: Int
+ $startCursor: String
+ $endCursor: String
+ ) @inContext(country: $country, language: $language) {
+ collection(handle: $handle) {
+ id
+ handle
+ title
+ description
+ products(
+ first: $first,
+ last: $last,
+ before: $startCursor,
+ after: $endCursor
+ ) {
+ nodes {
+ ...ProductItem
+ }
+ pageInfo {
+ hasPreviousPage
+ hasNextPage
+ endCursor
+ startCursor
+ }
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections._index.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections._index.tsx
new file mode 100644
index 000000000..b24ea672a
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections._index.tsx
@@ -0,0 +1,112 @@
+import { useLoaderData, Link } from '@remix-run/react'
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { getPaginationVariables, Image } from '@shopify/hydrogen'
+import type { CollectionFragment } from 'storefrontapi.generated'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, request }: LoaderFunctionArgs) {
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 4,
+ })
+
+ const [{ collections }] = await Promise.all([
+ context.storefront.query(COLLECTIONS_QUERY, {
+ variables: paginationVariables,
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ return { collections }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Collections() {
+ const { collections } = useLoaderData()
+
+ return (
+
+
Collections
+
+ {({ node: collection, index }) => }
+
+
+ )
+}
+
+function CollectionItem({ collection, index }: { collection: CollectionFragment; index: number }) {
+ return (
+
+ {collection?.image && (
+
+ )}
+ {collection.title}
+
+ )
+}
+
+const COLLECTIONS_QUERY = `#graphql
+ fragment Collection on Collection {
+ id
+ title
+ handle
+ image {
+ id
+ url
+ altText
+ width
+ height
+ }
+ }
+ query StoreCollections(
+ $country: CountryCode
+ $endCursor: String
+ $first: Int
+ $language: LanguageCode
+ $last: Int
+ $startCursor: String
+ ) @inContext(country: $country, language: $language) {
+ collections(
+ first: $first,
+ last: $last,
+ before: $startCursor,
+ after: $endCursor
+ ) {
+ nodes {
+ ...Collection
+ }
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections.all.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections.all.tsx
new file mode 100644
index 000000000..92dc49036
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/collections.all.tsx
@@ -0,0 +1,145 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, Link, type MetaFunction } from '@remix-run/react'
+import { getPaginationVariables, Image, Money } from '@shopify/hydrogen'
+import type { ProductItemFragment } from 'storefrontapi.generated'
+import { useVariantUrl } from '~/lib/variants'
+import { PaginatedResourceSection } from '~/components/PaginatedResourceSection'
+
+export const meta: MetaFunction = () => {
+ return [{ title: `Hydrogen | Products` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, request }: LoaderFunctionArgs) {
+ const { storefront } = context
+ const paginationVariables = getPaginationVariables(request, {
+ pageBy: 8,
+ })
+
+ const [{ products }] = await Promise.all([
+ storefront.query(CATALOG_QUERY, {
+ variables: { ...paginationVariables },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+ return { products }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Collection() {
+ const { products } = useLoaderData()
+
+ return (
+
+
Products
+
+ {({ node: product, index }) => (
+
+ )}
+
+
+ )
+}
+
+function ProductItem({ product, loading }: { product: ProductItemFragment; loading?: 'eager' | 'lazy' }) {
+ const variant = product.variants.nodes[0]
+ const variantUrl = useVariantUrl(product.handle, variant.selectedOptions)
+ return (
+
+ {product.featuredImage && (
+
+ )}
+ {product.title}
+
+
+
+
+ )
+}
+
+const PRODUCT_ITEM_FRAGMENT = `#graphql
+ fragment MoneyProductItem on MoneyV2 {
+ amount
+ currencyCode
+ }
+ fragment ProductItem on Product {
+ id
+ handle
+ title
+ featuredImage {
+ id
+ altText
+ url
+ width
+ height
+ }
+ priceRange {
+ minVariantPrice {
+ ...MoneyProductItem
+ }
+ maxVariantPrice {
+ ...MoneyProductItem
+ }
+ }
+ variants(first: 1) {
+ nodes {
+ selectedOptions {
+ name
+ value
+ }
+ }
+ }
+ }
+` as const
+
+// NOTE: https://shopify.dev/docs/api/storefront/2024-01/objects/product
+const CATALOG_QUERY = `#graphql
+ query Catalog(
+ $country: CountryCode
+ $language: LanguageCode
+ $first: Int
+ $last: Int
+ $startCursor: String
+ $endCursor: String
+ ) @inContext(country: $country, language: $language) {
+ products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
+ nodes {
+ ...ProductItem
+ }
+ pageInfo {
+ hasPreviousPage
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ }
+ }
+ ${PRODUCT_ITEM_FRAGMENT}
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/discount.$code.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/discount.$code.tsx
new file mode 100644
index 000000000..0decae16e
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/discount.$code.tsx
@@ -0,0 +1,46 @@
+import { redirect, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+
+/**
+ * Automatically applies a discount found on the url
+ * If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
+ *
+ * @example
+ * Example path applying a discount and optional redirecting (defaults to the home page)
+ * ```js
+ * /discount/FREESHIPPING?redirect=/products
+ *
+ * ```
+ */
+export async function loader({ request, context, params }: LoaderFunctionArgs) {
+ const { cart } = context
+ const { code } = params
+
+ const url = new URL(request.url)
+ const searchParams = new URLSearchParams(url.search)
+ let redirectParam = searchParams.get('redirect') || searchParams.get('return_to') || '/'
+
+ if (redirectParam.includes('//')) {
+ // Avoid redirecting to external URLs to prevent phishing attacks
+ redirectParam = '/'
+ }
+
+ searchParams.delete('redirect')
+ searchParams.delete('return_to')
+
+ const redirectUrl = `${redirectParam}?${searchParams}`
+
+ if (!code) {
+ return redirect(redirectUrl)
+ }
+
+ const result = await cart.updateDiscountCodes([code])
+ const headers = cart.setCartId(result.cart.id)
+
+ // Using set-cookie on a 303 redirect will not work if the domain origin have port number (:3000)
+ // If there is no cart id and a new cart id is created in the progress, it will not be set in the cookie
+ // on localhost:3000
+ return redirect(redirectUrl, {
+ status: 303,
+ headers,
+ })
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/pages.$handle.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/pages.$handle.tsx
new file mode 100644
index 000000000..c9a0956f7
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/pages.$handle.tsx
@@ -0,0 +1,84 @@
+import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, type MetaFunction } from '@remix-run/react'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.page.title ?? ''}` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, params }: LoaderFunctionArgs) {
+ if (!params.handle) {
+ throw new Error('Missing page handle')
+ }
+
+ const [{ page }] = await Promise.all([
+ context.storefront.query(PAGE_QUERY, {
+ variables: {
+ handle: params.handle,
+ },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ if (!page) {
+ throw new Response('Not Found', { status: 404 })
+ }
+
+ return {
+ page,
+ }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context }: LoaderFunctionArgs) {
+ return {}
+}
+
+export default function Page() {
+ const { page } = useLoaderData()
+
+ return (
+
+
+
+
+ )
+}
+
+const PAGE_QUERY = `#graphql
+ query Page(
+ $language: LanguageCode,
+ $country: CountryCode,
+ $handle: String!
+ )
+ @inContext(language: $language, country: $country) {
+ page(handle: $handle) {
+ id
+ title
+ body
+ seo {
+ description
+ title
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/policies.$handle.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/policies.$handle.tsx
new file mode 100644
index 000000000..1ddece7d4
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/policies.$handle.tsx
@@ -0,0 +1,89 @@
+import { json, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { Link, useLoaderData, type MetaFunction } from '@remix-run/react'
+import { type Shop } from '@shopify/hydrogen/storefront-api-types'
+
+type SelectedPolicies = keyof Pick
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.policy.title ?? ''}` }]
+}
+
+export async function loader({ params, context }: LoaderFunctionArgs) {
+ if (!params.handle) {
+ throw new Response('No handle was passed in', { status: 404 })
+ }
+
+ const policyName = params.handle.replace(/-([a-z])/g, (_: unknown, m1: string) =>
+ m1.toUpperCase(),
+ ) as SelectedPolicies
+
+ const data = await context.storefront.query(POLICY_CONTENT_QUERY, {
+ variables: {
+ privacyPolicy: false,
+ shippingPolicy: false,
+ termsOfService: false,
+ refundPolicy: false,
+ [policyName]: true,
+ language: context.storefront.i18n?.language,
+ },
+ })
+
+ const policy = data.shop?.[policyName]
+
+ if (!policy) {
+ throw new Response('Could not find the policy', { status: 404 })
+ }
+
+ return json({ policy })
+}
+
+export default function Policy() {
+ const { policy } = useLoaderData()
+
+ return (
+
+
+
+
+ ← Back to Policies
+
+
+
{policy.title}
+
+
+ )
+}
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/Shop
+const POLICY_CONTENT_QUERY = `#graphql
+ fragment Policy on ShopPolicy {
+ body
+ handle
+ id
+ title
+ url
+ }
+ query Policy(
+ $country: CountryCode
+ $language: LanguageCode
+ $privacyPolicy: Boolean!
+ $refundPolicy: Boolean!
+ $shippingPolicy: Boolean!
+ $termsOfService: Boolean!
+ ) @inContext(language: $language, country: $country) {
+ shop {
+ privacyPolicy @include(if: $privacyPolicy) {
+ ...Policy
+ }
+ shippingPolicy @include(if: $shippingPolicy) {
+ ...Policy
+ }
+ termsOfService @include(if: $termsOfService) {
+ ...Policy
+ }
+ refundPolicy @include(if: $refundPolicy) {
+ ...Policy
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/policies._index.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/policies._index.tsx
new file mode 100644
index 000000000..f7a71e0c9
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/policies._index.tsx
@@ -0,0 +1,63 @@
+import { json, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, Link } from '@remix-run/react'
+
+export async function loader({ context }: LoaderFunctionArgs) {
+ const data = await context.storefront.query(POLICIES_QUERY)
+ const policies = Object.values(data.shop || {})
+
+ if (!policies.length) {
+ throw new Response('No policies found', { status: 404 })
+ }
+
+ return json({ policies })
+}
+
+export default function Policies() {
+ const { policies } = useLoaderData()
+
+ return (
+
+
Policies
+
+ {policies.map((policy) => {
+ if (!policy) return null
+ return (
+
+ {policy.title}
+
+ )
+ })}
+
+
+ )
+}
+
+const POLICIES_QUERY = `#graphql
+ fragment PolicyItem on ShopPolicy {
+ id
+ title
+ handle
+ }
+ query Policies ($country: CountryCode, $language: LanguageCode)
+ @inContext(country: $country, language: $language) {
+ shop {
+ privacyPolicy {
+ ...PolicyItem
+ }
+ shippingPolicy {
+ ...PolicyItem
+ }
+ termsOfService {
+ ...PolicyItem
+ }
+ refundPolicy {
+ ...PolicyItem
+ }
+ subscriptionPolicy {
+ id
+ title
+ handle
+ }
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/products.$handle.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/products.$handle.tsx
new file mode 100644
index 000000000..46bb64f56
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/products.$handle.tsx
@@ -0,0 +1,267 @@
+import { Suspense } from 'react'
+import { defer, redirect, type LoaderFunctionArgs } from '@netlify/remix-runtime'
+import { Await, useLoaderData, type MetaFunction } from '@remix-run/react'
+import type { ProductFragment } from 'storefrontapi.generated'
+import { getSelectedProductOptions, Analytics, useOptimisticVariant } from '@shopify/hydrogen'
+import type { SelectedOption } from '@shopify/hydrogen/storefront-api-types'
+import { getVariantUrl } from '~/lib/variants'
+import { ProductPrice } from '~/components/ProductPrice'
+import { ProductImage } from '~/components/ProductImage'
+import { ProductForm } from '~/components/ProductForm'
+
+export const meta: MetaFunction = ({ data }) => {
+ return [{ title: `Hydrogen | ${data?.product.title ?? ''}` }]
+}
+
+export async function loader(args: LoaderFunctionArgs) {
+ // Start fetching non-critical data without blocking time to first byte
+ const deferredData = loadDeferredData(args)
+
+ // Await the critical data required to render initial state of the page
+ const criticalData = await loadCriticalData(args)
+
+ return defer({ ...deferredData, ...criticalData })
+}
+
+/**
+ * Load data necessary for rendering content above the fold. This is the critical data
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
+ */
+async function loadCriticalData({ context, params, request }: LoaderFunctionArgs) {
+ const { handle } = params
+ const { storefront } = context
+
+ if (!handle) {
+ throw new Error('Expected product handle to be defined')
+ }
+
+ const [{ product }] = await Promise.all([
+ storefront.query(PRODUCT_QUERY, {
+ variables: { handle, selectedOptions: getSelectedProductOptions(request) },
+ }),
+ // Add other queries here, so that they are loaded in parallel
+ ])
+
+ if (!product?.id) {
+ throw new Response(null, { status: 404 })
+ }
+
+ const firstVariant = product.variants.nodes[0]
+ const firstVariantIsDefault = Boolean(
+ firstVariant.selectedOptions.find(
+ (option: SelectedOption) => option.name === 'Title' && option.value === 'Default Title',
+ ),
+ )
+
+ if (firstVariantIsDefault) {
+ product.selectedVariant = firstVariant
+ } else {
+ // if no selected variant was returned from the selected options,
+ // we redirect to the first variant's url with it's selected options applied
+ if (!product.selectedVariant) {
+ throw redirectToFirstVariant({ product, request })
+ }
+ }
+
+ return {
+ product,
+ }
+}
+
+/**
+ * Load data for rendering content below the fold. This data is deferred and will be
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
+ * Make sure to not throw any errors here, as it will cause the page to 500.
+ */
+function loadDeferredData({ context, params }: LoaderFunctionArgs) {
+ // In order to show which variants are available in the UI, we need to query
+ // all of them. But there might be a *lot*, so instead separate the variants
+ // into it's own separate query that is deferred. So there's a brief moment
+ // where variant options might show as available when they're not, but after
+ // this deffered query resolves, the UI will update.
+ const variants = context.storefront
+ .query(VARIANTS_QUERY, {
+ variables: { handle: params.handle! },
+ })
+ .catch((error) => {
+ // Log query errors, but don't throw them so the page can still render
+ console.error(error)
+ return null
+ })
+
+ return {
+ variants,
+ }
+}
+
+function redirectToFirstVariant({ product, request }: { product: ProductFragment; request: Request }) {
+ const url = new URL(request.url)
+ const firstVariant = product.variants.nodes[0]
+
+ return redirect(
+ getVariantUrl({
+ pathname: url.pathname,
+ handle: product.handle,
+ selectedOptions: firstVariant.selectedOptions,
+ searchParams: new URLSearchParams(url.search),
+ }),
+ {
+ status: 302,
+ },
+ )
+}
+
+export default function Product() {
+ const { product, variants } = useLoaderData()
+ const selectedVariant = useOptimisticVariant(product.selectedVariant, variants)
+
+ const { title, descriptionHtml } = product
+
+ return (
+
+
+
+
{title}
+
+
+
}>
+
+ {(data) => (
+
+ )}
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+ )
+}
+
+const PRODUCT_VARIANT_FRAGMENT = `#graphql
+ fragment ProductVariant on ProductVariant {
+ availableForSale
+ compareAtPrice {
+ amount
+ currencyCode
+ }
+ id
+ image {
+ __typename
+ id
+ url
+ altText
+ width
+ height
+ }
+ price {
+ amount
+ currencyCode
+ }
+ product {
+ title
+ handle
+ }
+ selectedOptions {
+ name
+ value
+ }
+ sku
+ title
+ unitPrice {
+ amount
+ currencyCode
+ }
+ }
+` as const
+
+const PRODUCT_FRAGMENT = `#graphql
+ fragment Product on Product {
+ id
+ title
+ vendor
+ handle
+ descriptionHtml
+ description
+ options {
+ name
+ values
+ }
+ selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
+ ...ProductVariant
+ }
+ variants(first: 1) {
+ nodes {
+ ...ProductVariant
+ }
+ }
+ seo {
+ description
+ title
+ }
+ }
+ ${PRODUCT_VARIANT_FRAGMENT}
+` as const
+
+const PRODUCT_QUERY = `#graphql
+ query Product(
+ $country: CountryCode
+ $handle: String!
+ $language: LanguageCode
+ $selectedOptions: [SelectedOptionInput!]!
+ ) @inContext(country: $country, language: $language) {
+ product(handle: $handle) {
+ ...Product
+ }
+ }
+ ${PRODUCT_FRAGMENT}
+` as const
+
+const PRODUCT_VARIANTS_FRAGMENT = `#graphql
+ fragment ProductVariants on Product {
+ variants(first: 250) {
+ nodes {
+ ...ProductVariant
+ }
+ }
+ }
+ ${PRODUCT_VARIANT_FRAGMENT}
+` as const
+
+const VARIANTS_QUERY = `#graphql
+ ${PRODUCT_VARIANTS_FRAGMENT}
+ query ProductVariants(
+ $country: CountryCode
+ $language: LanguageCode
+ $handle: String!
+ ) @inContext(country: $country, language: $language) {
+ product(handle: $handle) {
+ ...ProductVariants
+ }
+ }
+` as const
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/routes/search.tsx b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/search.tsx
new file mode 100644
index 000000000..0dfd3982b
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/routes/search.tsx
@@ -0,0 +1,381 @@
+import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from '@netlify/remix-runtime'
+import { useLoaderData, type MetaFunction } from '@remix-run/react'
+import { getPaginationVariables, Analytics } from '@shopify/hydrogen'
+import { SearchForm } from '~/components/SearchForm'
+import { SearchResults } from '~/components/SearchResults'
+import { type RegularSearchReturn, type PredictiveSearchReturn, getEmptyPredictiveSearchResult } from '~/lib/search'
+
+export const meta: MetaFunction = () => {
+ return [{ title: `Hydrogen | Search` }]
+}
+
+export async function loader({ request, context }: LoaderFunctionArgs) {
+ const url = new URL(request.url)
+ const isPredictive = url.searchParams.has('predictive')
+ const searchPromise = isPredictive ? predictiveSearch({ request, context }) : regularSearch({ request, context })
+
+ searchPromise.catch((error: Error) => {
+ console.error(error)
+ return { term: '', result: null, error: error.message }
+ })
+
+ return json(await searchPromise)
+}
+
+/**
+ * Renders the /search route
+ */
+export default function SearchPage() {
+ const { type, term, result, error } = useLoaderData()
+ if (type === 'predictive') return null
+
+ return (
+
+
Search
+
+ {({ inputRef }) => (
+ <>
+
+
+ Search
+ >
+ )}
+
+ {error &&
{error}
}
+ {!term || !result?.total ? (
+
+ ) : (
+
+ {({ articles, pages, products, term }) => (
+
+
+
+
+
+ )}
+
+ )}
+
+
+ )
+}
+
+/**
+ * Regular search query and fragments
+ * (adjust as needed)
+ */
+const SEARCH_PRODUCT_FRAGMENT = `#graphql
+ fragment SearchProduct on Product {
+ __typename
+ handle
+ id
+ publishedAt
+ title
+ trackingParameters
+ vendor
+ variants(first: 1) {
+ nodes {
+ id
+ image {
+ url
+ altText
+ width
+ height
+ }
+ price {
+ amount
+ currencyCode
+ }
+ compareAtPrice {
+ amount
+ currencyCode
+ }
+ selectedOptions {
+ name
+ value
+ }
+ product {
+ handle
+ title
+ }
+ }
+ }
+ }
+` as const
+
+const SEARCH_PAGE_FRAGMENT = `#graphql
+ fragment SearchPage on Page {
+ __typename
+ handle
+ id
+ title
+ trackingParameters
+ }
+` as const
+
+const SEARCH_ARTICLE_FRAGMENT = `#graphql
+ fragment SearchArticle on Article {
+ __typename
+ handle
+ id
+ title
+ trackingParameters
+ }
+` as const
+
+const PAGE_INFO_FRAGMENT = `#graphql
+ fragment PageInfoFragment on PageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
+` as const
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/search
+export const SEARCH_QUERY = `#graphql
+ query RegularSearch(
+ $country: CountryCode
+ $endCursor: String
+ $first: Int
+ $language: LanguageCode
+ $last: Int
+ $term: String!
+ $startCursor: String
+ ) @inContext(country: $country, language: $language) {
+ articles: search(
+ query: $term,
+ types: [ARTICLE],
+ first: $first,
+ ) {
+ nodes {
+ ...on Article {
+ ...SearchArticle
+ }
+ }
+ }
+ pages: search(
+ query: $term,
+ types: [PAGE],
+ first: $first,
+ ) {
+ nodes {
+ ...on Page {
+ ...SearchPage
+ }
+ }
+ }
+ products: search(
+ after: $endCursor,
+ before: $startCursor,
+ first: $first,
+ last: $last,
+ query: $term,
+ sortKey: RELEVANCE,
+ types: [PRODUCT],
+ unavailableProducts: HIDE,
+ ) {
+ nodes {
+ ...on Product {
+ ...SearchProduct
+ }
+ }
+ pageInfo {
+ ...PageInfoFragment
+ }
+ }
+ }
+ ${SEARCH_PRODUCT_FRAGMENT}
+ ${SEARCH_PAGE_FRAGMENT}
+ ${SEARCH_ARTICLE_FRAGMENT}
+ ${PAGE_INFO_FRAGMENT}
+` as const
+
+/**
+ * Regular search fetcher
+ */
+async function regularSearch({
+ request,
+ context,
+}: Pick): Promise {
+ const { storefront } = context
+ const url = new URL(request.url)
+ const variables = getPaginationVariables(request, { pageBy: 8 })
+ const term = String(url.searchParams.get('q') || '')
+
+ // Search articles, pages, and products for the `q` term
+ const { errors, ...items } = await storefront.query(SEARCH_QUERY, {
+ variables: { ...variables, term },
+ })
+
+ if (!items) {
+ throw new Error('No search data returned from Shopify API')
+ }
+
+ const total = Object.values(items).reduce((acc, { nodes }) => acc + nodes.length, 0)
+
+ const error = errors ? errors.map(({ message }) => message).join(', ') : undefined
+
+ return { type: 'regular', term, error, result: { total, items } }
+}
+
+/**
+ * Predictive search query and fragments
+ * (adjust as needed)
+ */
+const PREDICTIVE_SEARCH_ARTICLE_FRAGMENT = `#graphql
+ fragment PredictiveArticle on Article {
+ __typename
+ id
+ title
+ handle
+ blog {
+ handle
+ }
+ image {
+ url
+ altText
+ width
+ height
+ }
+ trackingParameters
+ }
+` as const
+
+const PREDICTIVE_SEARCH_COLLECTION_FRAGMENT = `#graphql
+ fragment PredictiveCollection on Collection {
+ __typename
+ id
+ title
+ handle
+ image {
+ url
+ altText
+ width
+ height
+ }
+ trackingParameters
+ }
+` as const
+
+const PREDICTIVE_SEARCH_PAGE_FRAGMENT = `#graphql
+ fragment PredictivePage on Page {
+ __typename
+ id
+ title
+ handle
+ trackingParameters
+ }
+` as const
+
+const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql
+ fragment PredictiveProduct on Product {
+ __typename
+ id
+ title
+ handle
+ trackingParameters
+ variants(first: 1) {
+ nodes {
+ id
+ image {
+ url
+ altText
+ width
+ height
+ }
+ price {
+ amount
+ currencyCode
+ }
+ }
+ }
+ }
+` as const
+
+const PREDICTIVE_SEARCH_QUERY_FRAGMENT = `#graphql
+ fragment PredictiveQuery on SearchQuerySuggestion {
+ __typename
+ text
+ styledText
+ trackingParameters
+ }
+` as const
+
+// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/predictiveSearch
+const PREDICTIVE_SEARCH_QUERY = `#graphql
+ query PredictiveSearch(
+ $country: CountryCode
+ $language: LanguageCode
+ $limit: Int!
+ $limitScope: PredictiveSearchLimitScope!
+ $term: String!
+ $types: [PredictiveSearchType!]
+ ) @inContext(country: $country, language: $language) {
+ predictiveSearch(
+ limit: $limit,
+ limitScope: $limitScope,
+ query: $term,
+ types: $types,
+ ) {
+ articles {
+ ...PredictiveArticle
+ }
+ collections {
+ ...PredictiveCollection
+ }
+ pages {
+ ...PredictivePage
+ }
+ products {
+ ...PredictiveProduct
+ }
+ queries {
+ ...PredictiveQuery
+ }
+ }
+ }
+ ${PREDICTIVE_SEARCH_ARTICLE_FRAGMENT}
+ ${PREDICTIVE_SEARCH_COLLECTION_FRAGMENT}
+ ${PREDICTIVE_SEARCH_PAGE_FRAGMENT}
+ ${PREDICTIVE_SEARCH_PRODUCT_FRAGMENT}
+ ${PREDICTIVE_SEARCH_QUERY_FRAGMENT}
+` as const
+
+/**
+ * Predictive search fetcher
+ */
+async function predictiveSearch({
+ request,
+ context,
+}: Pick): Promise {
+ const { storefront } = context
+ const url = new URL(request.url)
+ const term = String(url.searchParams.get('q') || '').trim()
+ const limit = Number(url.searchParams.get('limit') || 10)
+ const type = 'predictive'
+
+ if (!term) return { type, term, result: getEmptyPredictiveSearchResult() }
+
+ // Predictively search articles, collections, pages, products, and queries (suggestions)
+ const { predictiveSearch: items, errors } = await storefront.query(PREDICTIVE_SEARCH_QUERY, {
+ variables: {
+ // customize search options as needed
+ limit,
+ limitScope: 'EACH',
+ term,
+ },
+ })
+
+ if (errors) {
+ throw new Error(`Shopify API errors: ${errors.map(({ message }) => message).join(', ')}`)
+ }
+
+ if (!items) {
+ throw new Error('No predictive search data returned from Shopify API')
+ }
+
+ const total = Object.values(items).reduce((acc, item) => acc + item.length, 0)
+
+ return { type, term, result: { items, total } }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/styles/app.css b/tests/e2e/fixtures/hydrogen-vite-site/app/styles/app.css
new file mode 100644
index 000000000..c9938e4d5
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/styles/app.css
@@ -0,0 +1,483 @@
+:root {
+ --aside-width: 400px;
+ --cart-aside-summary-height-with-discount: 300px;
+ --cart-aside-summary-height: 250px;
+ --grid-item-width: 355px;
+ --header-height: 64px;
+ --color-dark: #000;
+ --color-light: #fff;
+}
+
+img {
+ border-radius: 4px;
+}
+
+/*
+* --------------------------------------------------
+* components/Aside
+* --------------------------------------------------
+*/
+aside {
+ background: var(--color-light);
+ box-shadow: 0 0 50px rgba(0, 0, 0, 0.3);
+ height: 100vh;
+ max-width: var(--aside-width);
+ min-width: var(--aside-width);
+ position: fixed;
+ right: calc(-1 * var(--aside-width));
+ top: 0;
+ transition: transform 200ms ease-in-out;
+}
+
+aside header {
+ align-items: center;
+ border-bottom: 1px solid var(--color-dark);
+ display: flex;
+ height: var(--header-height);
+ justify-content: space-between;
+ padding: 0 20px;
+}
+
+aside header h3 {
+ margin: 0;
+}
+
+aside header .close {
+ font-weight: bold;
+ opacity: 0.8;
+ text-decoration: none;
+ transition: all 200ms;
+ width: 20px;
+}
+
+aside header .close:hover {
+ opacity: 1;
+}
+
+aside header h2 {
+ margin-bottom: 0.6rem;
+ margin-top: 0;
+}
+
+aside main {
+ margin: 1rem;
+}
+
+aside p {
+ margin: 0 0 0.25rem;
+}
+
+aside p:last-child {
+ margin: 0;
+}
+
+aside li {
+ margin-bottom: 0.125rem;
+}
+
+.overlay {
+ background: rgba(0, 0, 0, 0.2);
+ bottom: 0;
+ left: 0;
+ opacity: 0;
+ pointer-events: none;
+ position: fixed;
+ right: 0;
+ top: 0;
+ transition: opacity 400ms ease-in-out;
+ transition: opacity 400ms;
+ visibility: hidden;
+ z-index: 10;
+}
+
+.overlay .close-outside {
+ background: transparent;
+ border: none;
+ color: transparent;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: calc(100% - var(--aside-width));
+}
+
+.overlay .light {
+ background: rgba(255, 255, 255, 0.5);
+}
+
+.overlay .cancel {
+ cursor: default;
+ height: 100%;
+ position: absolute;
+ width: 100%;
+}
+
+.overlay.expanded {
+ opacity: 1;
+ pointer-events: auto;
+ visibility: visible;
+}
+
+/* reveal aside */
+.overlay.expanded aside {
+ transform: translateX(calc(var(--aside-width) * -1));
+}
+
+button.reset {
+ border: 0;
+ background: inherit;
+ font-size: inherit;
+}
+
+button.reset > * {
+ margin: 0;
+}
+
+button.reset:not(:has(> *)) {
+ height: 1.5rem;
+ line-height: 1.5rem;
+}
+
+button.reset:hover:not(:has(> *)) {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+/*
+* --------------------------------------------------
+* components/Header
+* --------------------------------------------------
+*/
+.header {
+ align-items: center;
+ background: #fff;
+ display: flex;
+ height: var(--header-height);
+ padding: 0 1rem;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.header-menu-mobile-toggle {
+ @media (min-width: 48em) {
+ display: none;
+ }
+}
+
+.header-menu-mobile {
+ display: flex;
+ flex-direction: column;
+ grid-gap: 1rem;
+}
+
+.header-menu-desktop {
+ display: none;
+ grid-gap: 1rem;
+
+ @media (min-width: 45em) {
+ display: flex;
+ grid-gap: 1rem;
+ margin-left: 3rem;
+ }
+}
+
+.header-menu-item {
+ cursor: pointer;
+}
+
+.header-ctas {
+ align-items: center;
+ display: flex;
+ grid-gap: 1rem;
+ margin-left: auto;
+}
+
+/*
+* --------------------------------------------------
+* components/Footer
+* --------------------------------------------------
+*/
+.footer {
+ background: var(--color-dark);
+ margin-top: auto;
+}
+
+.footer-menu {
+ align-items: center;
+ display: flex;
+ grid-gap: 1rem;
+ padding: 1rem;
+}
+
+.footer-menu a {
+ color: var(--color-light);
+}
+
+/*
+* --------------------------------------------------
+* components/Cart
+* --------------------------------------------------
+*/
+.cart-main {
+ height: 100%;
+ max-height: calc(100vh - var(--cart-aside-summary-height));
+ overflow-y: auto;
+ width: auto;
+}
+
+.cart-main.with-discount {
+ max-height: calc(100vh - var(--cart-aside-summary-height-with-discount));
+}
+
+.cart-line {
+ display: flex;
+ padding: 0.75rem 0;
+}
+
+.cart-line img {
+ height: 100%;
+ display: block;
+ margin-right: 0.75rem;
+}
+
+.cart-summary-page {
+ position: relative;
+}
+
+.cart-summary-aside {
+ background: white;
+ border-top: 1px solid var(--color-dark);
+ bottom: 0;
+ padding-top: 0.75rem;
+ position: absolute;
+ width: calc(var(--aside-width) - 40px);
+}
+
+.cart-line-quantity {
+ display: flex;
+}
+
+.cart-discount {
+ align-items: center;
+ display: flex;
+ margin-top: 0.25rem;
+}
+
+.cart-subtotal {
+ align-items: center;
+ display: flex;
+}
+
+/*
+* --------------------------------------------------
+* components/Search
+* --------------------------------------------------
+*/
+.predictive-search {
+ height: calc(100vh - var(--header-height) - 40px);
+ overflow-y: auto;
+}
+
+.predictive-search-form {
+ background: var(--color-light);
+ position: sticky;
+ top: 0;
+}
+
+.predictive-search-result {
+ margin-bottom: 2rem;
+}
+
+.predictive-search-result h5 {
+ text-transform: uppercase;
+}
+
+.predictive-search-result-item {
+ margin-bottom: 0.5rem;
+}
+
+.predictive-search-result-item a {
+ align-items: center;
+ display: flex;
+}
+
+.predictive-search-result-item a img {
+ margin-right: 0.75rem;
+ height: 100%;
+}
+
+.search-result {
+ margin-bottom: 1.5rem;
+}
+
+.search-results-item {
+ margin-bottom: 0.5rem;
+}
+
+.search-results-item a {
+ display: flex;
+ flex: row;
+ align-items: center;
+ gap: 1rem;
+}
+
+/*
+* --------------------------------------------------
+* routes/__index
+* --------------------------------------------------
+*/
+.featured-collection {
+ display: block;
+ margin-bottom: 2rem;
+ position: relative;
+}
+
+.featured-collection-image {
+ aspect-ratio: 1 / 1;
+
+ @media (min-width: 45em) {
+ aspect-ratio: 16 / 9;
+ }
+}
+
+.featured-collection img {
+ height: auto;
+ max-height: 100%;
+ object-fit: cover;
+}
+
+.recommended-products-grid {
+ display: grid;
+ grid-gap: 1.5rem;
+ grid-template-columns: repeat(2, 1fr);
+
+ @media (min-width: 45em) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+}
+
+.recommended-product img {
+ height: auto;
+}
+
+/*
+* --------------------------------------------------
+* routes/collections._index.tsx
+* --------------------------------------------------
+*/
+.collections-grid {
+ display: grid;
+ grid-gap: 1.5rem;
+ grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
+ margin-bottom: 2rem;
+}
+
+.collection-item img {
+ height: auto;
+}
+
+/*
+* --------------------------------------------------
+* routes/collections.$handle.tsx
+* --------------------------------------------------
+*/
+.collection-description {
+ margin-bottom: 1rem;
+ max-width: 95%;
+
+ @media (min-width: 45em) {
+ max-width: 600px;
+ }
+}
+
+.products-grid {
+ display: grid;
+ grid-gap: 1.5rem;
+ grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
+ margin-bottom: 2rem;
+}
+
+.product-item img {
+ height: auto;
+ width: 100%;
+}
+
+/*
+* --------------------------------------------------
+* routes/products.$handle.tsx
+* --------------------------------------------------
+*/
+.product {
+ display: grid;
+
+ @media (min-width: 45em) {
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 4rem;
+ }
+}
+
+.product h1 {
+ margin-top: 0;
+}
+
+.product-image img {
+ height: auto;
+ width: 100%;
+}
+
+.product-main {
+ align-self: start;
+ position: sticky;
+ top: 6rem;
+}
+
+.product-price-on-sale {
+ display: flex;
+ grid-gap: 0.5rem;
+}
+
+.product-price-on-sale s {
+ opacity: 0.5;
+}
+
+.product-options-grid {
+ display: flex;
+ flex-wrap: wrap;
+ grid-gap: 0.75rem;
+}
+
+.product-options-item {
+ padding: 0.25rem 0.5rem;
+}
+
+/*
+* --------------------------------------------------
+* routes/blog._index.tsx
+* --------------------------------------------------
+*/
+.blog-grid {
+ display: grid;
+ grid-gap: 1.5rem;
+ grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
+ margin-bottom: 2rem;
+}
+
+.blog-article-image {
+ aspect-ratio: 3/2;
+ display: block;
+}
+
+.blog-article-image img {
+ height: 100%;
+}
+
+/*
+* --------------------------------------------------
+* routes/blog.$articlehandle.tsx
+* --------------------------------------------------
+*/
+.article img {
+ height: auto;
+ width: 100%;
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/app/styles/reset.css b/tests/e2e/fixtures/hydrogen-vite-site/app/styles/reset.css
new file mode 100644
index 000000000..4488766b3
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/app/styles/reset.css
@@ -0,0 +1,139 @@
+body {
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ 'Open Sans',
+ 'Helvetica Neue',
+ sans-serif;
+ margin: 0;
+ padding: 0;
+}
+
+h1,
+h2,
+p {
+ margin: 0;
+ padding: 0;
+}
+
+h1 {
+ font-size: 1.6rem;
+ font-weight: 700;
+ line-height: 1.4;
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+}
+
+h2 {
+ font-size: 1.2rem;
+ font-weight: 700;
+ line-height: 1.4;
+ margin-bottom: 1rem;
+}
+
+h4 {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+h5 {
+ margin-bottom: 1rem;
+ margin-top: 0.5rem;
+}
+
+p {
+ font-size: 1rem;
+ line-height: 1.4;
+}
+
+a {
+ color: #000;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+hr {
+ border-bottom: none;
+ border-top: 1px solid #000;
+ margin: 0;
+}
+
+pre {
+ white-space: pre-wrap;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+body > main {
+ margin: 0 1rem 1rem 1rem;
+}
+
+section {
+ padding: 1rem 0;
+ @media (min-width: 768px) {
+ padding: 2rem 0;
+ }
+}
+
+fieldset {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 0.5rem;
+ padding: 1rem;
+}
+
+form {
+ max-width: 100%;
+ @media (min-width: 768px) {
+ max-width: 400px;
+ }
+}
+
+input {
+ border-radius: 4px;
+ border: 1px solid #000;
+ font-size: 1rem;
+ margin-bottom: 0.5rem;
+ margin-top: 0.25rem;
+ padding: 0.5rem;
+}
+
+legend {
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+li {
+ margin-bottom: 0.5rem;
+}
+
+dl {
+ margin: 0.5rem 0;
+}
+
+code {
+ background: #ddd;
+ border-radius: 4px;
+ font-family: monospace;
+ padding: 0.25rem;
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/env.d.ts b/tests/e2e/fixtures/hydrogen-vite-site/env.d.ts
new file mode 100644
index 000000000..f3e4328fc
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/env.d.ts
@@ -0,0 +1,28 @@
+///
+
+// Enhance TypeScript's built-in typings.
+import '@total-typescript/ts-reset'
+
+import type { HydrogenContext, HydrogenSessionData, HydrogenEnv } from '@shopify/hydrogen'
+import type { createAppLoadContext } from '~/lib/context'
+
+declare global {
+ /**
+ * A global `process` object is only available during build to access NODE_ENV.
+ */
+ const process: { env: { NODE_ENV: 'production' | 'development' } }
+
+ interface Env extends HydrogenEnv {
+ // declare additional Env parameter use in the fetch handler and Remix loader context here
+ }
+}
+
+declare module '@netlify/remix-runtime' {
+ interface AppLoadContext extends Awaited> {
+ // to change context type, change the return of createAppLoadContext() instead
+ }
+
+ interface SessionData extends HydrogenSessionData {
+ // declare local additions to the Remix session data here
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/netlify.toml b/tests/e2e/fixtures/hydrogen-vite-site/netlify.toml
new file mode 100644
index 000000000..462faa0e8
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/netlify.toml
@@ -0,0 +1,15 @@
+[build]
+ command = "npm run build"
+ publish = "dist/client"
+
+# Set immutable caching for static files, because they have fingerprinted filenames
+[[headers]]
+for = "/build/*"
+[headers.values]
+"Cache-Control" = "public, max-age=31560000, immutable"
+
+# These are only used to set up the template, and are not used in the build
+# If you want to update the real values, change them in the site UI or CLI
+[template.environment]
+PUBLIC_STORE_DOMAIN = "Store domain. Leave as 'mock.shop' to try the demo site"
+SESSION_SECRET = "Session secret - change to a random value for production"
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/package.json b/tests/e2e/fixtures/hydrogen-vite-site/package.json
new file mode 100644
index 000000000..1d9b3afef
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "hydrogen-storefront",
+ "private": true,
+ "sideEffects": false,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "build": "remix vite:build",
+ "codegen": "shopify hydrogen codegen",
+ "dev": "shopify hydrogen dev --codegen",
+ "lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx .",
+ "preview": "netlify serve",
+ "typecheck": "tsc"
+ },
+ "dependencies": {
+ "@netlify/edge-functions": "^2.10.0",
+ "@netlify/remix-edge-adapter": "^3.3.0",
+ "@netlify/remix-runtime": "^2.3.0",
+ "@remix-run/react": "^2.11.2",
+ "@shopify/hydrogen": "^2024.7.4",
+ "graphql": "^16.6.0",
+ "graphql-tag": "^2.12.6",
+ "isbot": "^5.1.17",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@graphql-codegen/cli": "^5.0.2",
+ "@remix-run/dev": "^2.11.2",
+ "@remix-run/eslint-config": "^2.11.2",
+ "@shopify/cli": "^3.66.1",
+ "@shopify/hydrogen-codegen": "^0.3.1",
+ "@shopify/prettier-config": "^1.1.2",
+ "@total-typescript/ts-reset": "^0.4.2",
+ "@types/eslint": "^8.4.10",
+ "@types/react": "^18.2.22",
+ "@types/react-dom": "^18.2.7",
+ "eslint": "^8.20.0",
+ "eslint-plugin-hydrogen": "0.12.2",
+ "prettier": "^2.8.4",
+ "typescript": "^5.2.2",
+ "vite": "^5.4.2",
+ "vite-tsconfig-paths": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/public/favicon.svg b/tests/e2e/fixtures/hydrogen-vite-site/public/favicon.svg
new file mode 100644
index 000000000..f6c649733
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/public/favicon.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/server.ts b/tests/e2e/fixtures/hydrogen-vite-site/server.ts
new file mode 100644
index 000000000..b34191036
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/server.ts
@@ -0,0 +1,44 @@
+// @ts-ignore -- virtual entry point for the app, resolved by Vite at build time
+import * as remixBuild from 'virtual:remix/server-build'
+import type { Context } from '@netlify/edge-functions'
+import { createHydrogenAppLoadContext, createRequestHandler } from '@netlify/remix-edge-adapter'
+import { storefrontRedirect } from '@shopify/hydrogen'
+import { createAppLoadContext } from '~/lib/context'
+
+export default async function (request: Request, netlifyContext: Context): Promise {
+ try {
+ const appLoadContext = await createHydrogenAppLoadContext(request, netlifyContext, createAppLoadContext)
+ const handleRequest = createRequestHandler({
+ build: remixBuild,
+ mode: process.env.NODE_ENV,
+ })
+
+ const response = await handleRequest(request, appLoadContext)
+
+ if (!response) {
+ return
+ }
+
+ if (appLoadContext.session.isPending) {
+ response.headers.set('Set-Cookie', await appLoadContext.session.commit())
+ }
+
+ 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: appLoadContext.storefront,
+ })
+ }
+
+ return response
+ } catch (error) {
+ console.error(error)
+ return new Response('An unexpected error occurred', { status: 500 })
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/tsconfig.json b/tests/e2e/fixtures/hydrogen-vite-site/tsconfig.json
new file mode 100644
index 000000000..631280351
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "module": "ES2022",
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["app/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/tests/e2e/fixtures/hydrogen-vite-site/vite.config.js b/tests/e2e/fixtures/hydrogen-vite-site/vite.config.js
new file mode 100644
index 000000000..c5d47daf9
--- /dev/null
+++ b/tests/e2e/fixtures/hydrogen-vite-site/vite.config.js
@@ -0,0 +1,41 @@
+import { defineConfig } from 'vite'
+import { hydrogen } from '@shopify/hydrogen/vite'
+import { netlifyPlugin } from '@netlify/remix-edge-adapter/plugin'
+import { vitePlugin as remix } from '@remix-run/dev'
+import tsconfigPaths from 'vite-tsconfig-paths'
+
+export default defineConfig({
+ plugins: [
+ hydrogen(),
+ remix({
+ presets: [hydrogen.preset()],
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ },
+ }),
+ netlifyPlugin(),
+ tsconfigPaths(),
+ ],
+ build: {
+ // Allow a strict Content-Security-Policy
+ // withtout inlining assets as base64:
+ assetsInlineLimit: 0,
+ },
+ ssr: {
+ optimizeDeps: {
+ /**
+ * Include dependencies here if they throw CJS<>ESM errors.
+ * For example, for the following error:
+ *
+ * > ReferenceError: module is not defined
+ * > at /Users/.../node_modules/example-dep/index.js:1:1
+ *
+ * Include 'example-dep' in the array below.
+ * @see https://vitejs.dev/config/dep-optimization-options
+ */
+ include: [],
+ },
+ },
+})
diff --git a/tests/e2e/support/fixtures.ts b/tests/e2e/support/fixtures.ts
index aea280355..4e38aefe1 100644
--- a/tests/e2e/support/fixtures.ts
+++ b/tests/e2e/support/fixtures.ts
@@ -21,6 +21,22 @@ interface WorkerFixtures {
* A "classic" (non-Vite) Remix site using edge SSR
*/
classicEdgeSite: Fixture
+ /**
+ * A Hydrogen v2 site using Vite Remix and edge SSR
+ *
+ * NOTE: `PUBLIC_STORE_DOMAIN` and `SESSION_SECRET` are populated for this fixture
+ */
+ hydrogenViteSite: Fixture
+ /**
+ * A Hydrogen v2 site using Vite Remix and edge SSR, but invalid for use with Netlify packages
+ * because it is missing a `server.ts` (or .js, etc.) SSR "entrypoint" file.
+ *
+ * As we intend for this to fail to build at all, the fixture resolves to an Error (or `null` if
+ * it didn't fail).
+ *
+ * NOTE: `PUBLIC_STORE_DOMAIN` and `SESSION_SECRET` are populated for this fixture
+ */
+ hydrogenViteSiteNoEntrypoint: Error | null
}
export const test = base.extend({
@@ -52,5 +68,23 @@ export const test = base.extend({
},
{ scope: 'worker' },
],
+ hydrogenViteSite: [
+ async ({}, use) => {
+ const fixture = await deployFixture('hydrogen-vite-site')
+ await use(fixture)
+ },
+ { scope: 'worker' },
+ ],
+ hydrogenViteSiteNoEntrypoint: [
+ async ({}, use) => {
+ try {
+ await deployFixture('hydrogen-vite-site-no-entrypoint')
+ await use(null)
+ } catch (err: unknown) {
+ await use(err as Error)
+ }
+ },
+ { scope: 'worker' },
+ ],
})
export { expect } from '@playwright/test'
diff --git a/tests/e2e/user-journeys.spec.ts b/tests/e2e/user-journeys.spec.ts
index 518f9bad9..cf75fba33 100644
--- a/tests/e2e/user-journeys.spec.ts
+++ b/tests/e2e/user-journeys.spec.ts
@@ -120,6 +120,26 @@ test.describe('User journeys', () => {
})
})
+ test.describe('Hydrogen Vite site', () => {
+ test('serves a response from the edge when using @netlify/remix-edge-adapter and a root `server.ts`', async ({
+ page,
+ hydrogenViteSite,
+ }) => {
+ const response = await page.goto(hydrogenViteSite.url)
+ expect(response?.status()).toBe(200)
+ await expect(page.getByText('Mock.shop')).toBeVisible()
+ await expect(page.getByText('Recommended Products')).toBeVisible()
+ expect(response?.headers()['x-nf-edge-functions']).toBe('remix-server')
+ })
+
+ test('fails the build with an actionable message if the site is missing a root `server.ts` or similar', async ({
+ hydrogenViteSiteNoEntrypoint,
+ }) => {
+ expect(hydrogenViteSiteNoEntrypoint).toBeInstanceOf(Error)
+ expect(hydrogenViteSiteNoEntrypoint?.message).toMatch(/Your Hydrogen site must include a `server.ts`/)
+ })
+ })
+
test('response has user-defined Cache-Control header when using origin SSR', async ({ page, serverlessSite }) => {
const response = await page.goto(`${serverlessSite.url}/headers`)
await expect(page.getByRole('heading', { name: /Headers/i })).toBeVisible()