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) => ( + <> + + + + )} + + ) +} 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 ( +
+ + +
{children}
+ +
+ ) +} + +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 && {title}} + +
    + { + 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 ( + + + + ) +} + +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 ( +
    +
    + ) +} + +function CartEmpty({ hidden = false }: { hidden: boolean; layout?: CartMainProps['layout'] }) { + const { close } = useAside() + return ( + + ) +} 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 */} + + + {/* Show an input to apply a discount */} + +
    + +   + +
    +
    +
    + ) +} + +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 ( + + ) +} + +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 ( +
    + + {shop.name} + + + +
    + ) +} + +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 ( + + ) +} + +function HeaderCtas({ cart }: Pick) { + return ( + + ) +} + +function HeaderMenuMobileToggle() { + const { open } = useAside() + return ( + + ) +} + +function SearchToggle() { + const { open } = useAside() + return ( + + ) +} + +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 ( + + ) +} + +function SearchAside() { + return ( + + ) +} + +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 ( +
    + {image.altText +
    + ) +} 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}) => ( + * <> + * + * + * + * )} + * + */ +export function SearchForm({ children, ...props }: SearchFormProps) { + const inputRef = useRef(null) + + useFocusOnCmdK(inputRef) + + if (typeof children !== 'function') { + return null + } + + return ( +
    + {children({ inputRef })} +
    + ) +} + +/** + * 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} + )} +
    +

    {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.image.altText + )} +
      + {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.image.altText + )} +
      + {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 && {image.altText} +
      +

      {product.title}

      + + {product?.variants?.nodes?.[0].price && } + +
      + +
    • + ) + })} +
    +
    + ) +} + +function SearchResultsPredictiveQueries({ queries, inputRef }: PartialPredictiveSearchResult<'queries', 'inputRef'>) { + if (!queries.length) return null + + return ( +
    +
    Queries
    +
      + {queries.map((suggestion) => { + if (!suggestion) return null + + return ( +
    • +
      { + if (!inputRef.current) return + inputRef.current.value = suggestion.text + inputRef.current.focus() + }} + dangerouslySetInnerHTML={{ + __html: suggestion?.styledText, + }} + /> +
    • + ) + })} +
    +
    + ) +} + +function SearchResultsPredictiveEmpty({ term }: { term: React.MutableRefObject }) { + if (!term.current) { + return null + } + + return ( +

    + No results found for {term.current} +

    + ) +} + +/** + * Hook that returns the predictive search results and fetcher and input ref. + * @example + * '''ts + * const { items, total, inputRef, term, fetcher } = usePredictiveSearch(); + * ''' + **/ +function usePredictiveSearch(): UsePredictiveSearchReturn { + const fetcher = useFetcher({ key: 'search' }) + const term = useRef('') + const inputRef = useRef(null) + + if (fetcher?.state === 'loading') { + term.current = String(fetcher.formData?.get('q') || '') + } + + // capture the search input element as a ref + useEffect(() => { + if (!inputRef.current) { + inputRef.current = document.querySelector('input[type="search"]') + } + }, []) + + const { items, total } = fetcher?.data?.result ?? getEmptyPredictiveSearchResult() + + return { items, total, inputRef, term, fetcher } +} diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/entry.client.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/entry.client.tsx new file mode 100644 index 000000000..2c0fc8cb7 --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/entry.client.tsx @@ -0,0 +1,14 @@ +import { RemixBrowser } from '@remix-run/react' +import { startTransition, StrictMode } from 'react' +import { hydrateRoot } from 'react-dom/client' + +if (!window.location.origin.includes('webcache.googleusercontent.com')) { + startTransition(() => { + hydrateRoot( + document, + + + , + ) + }) +} diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/entry.server.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/entry.server.tsx new file mode 100644 index 000000000..71b7ae2ed --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/entry.server.tsx @@ -0,0 +1,2 @@ +// @ts-ignore -- This is a Vite virtual module. It will be resolved at build time. +export { default } from 'virtual:netlify-server-entry' diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/context.ts b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/context.ts new file mode 100644 index 000000000..aea8299d0 --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/context.ts @@ -0,0 +1,46 @@ +import { + createHydrogenContext, + type HydrogenContext, + InMemoryCache, +} from '@shopify/hydrogen'; +import {AppSession} from '~/lib/session'; +import {CART_QUERY_FRAGMENT} from '~/lib/fragments'; + +/** + * The context implementation is separate from server.ts + * so that type can be extracted for AppLoadContext + */ +export async function createAppLoadContext( + request: Request, + env: Env, + executionContext: ExecutionContext, +): Promise { + /** + * Open a cache instance in the worker and a custom session instance. + */ + if (!env?.SESSION_SECRET) { + throw new Error('SESSION_SECRET environment variable is not set'); + } + + const session = await AppSession.init(request, [env.SESSION_SECRET]); + + const hydrogenContext = createHydrogenContext({ + env, + request, + cache: new InMemoryCache(), + waitUntil: executionContext.waitUntil, + session, + i18n: { + language: 'EN', + country: 'US', + }, + cart: { + queryFragment: CART_QUERY_FRAGMENT, + }, + }); + + return { + ...hydrogenContext, + // add your custom context here + }; +} diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/fragments.ts b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/fragments.ts new file mode 100644 index 000000000..dc4426a9f --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/fragments.ts @@ -0,0 +1,233 @@ +// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/cart +export const CART_QUERY_FRAGMENT = `#graphql + fragment Money on MoneyV2 { + currencyCode + amount + } + fragment CartLine on CartLine { + id + quantity + attributes { + key + value + } + cost { + totalAmount { + ...Money + } + amountPerQuantity { + ...Money + } + compareAtAmountPerQuantity { + ...Money + } + } + merchandise { + ... on ProductVariant { + id + availableForSale + compareAtPrice { + ...Money + } + price { + ...Money + } + requiresShipping + title + image { + id + url + altText + width + height + + } + product { + handle + title + id + vendor + } + selectedOptions { + name + value + } + } + } + } + fragment CartLineComponent on ComponentizableCartLine { + id + quantity + attributes { + key + value + } + cost { + totalAmount { + ...Money + } + amountPerQuantity { + ...Money + } + compareAtAmountPerQuantity { + ...Money + } + } + merchandise { + ... on ProductVariant { + id + availableForSale + compareAtPrice { + ...Money + } + price { + ...Money + } + requiresShipping + title + image { + id + url + altText + width + height + } + product { + handle + title + id + vendor + } + selectedOptions { + name + value + } + } + } + } + fragment CartApiQuery on Cart { + updatedAt + id + appliedGiftCards { + lastCharacters + amountUsed { + ...Money + } + } + checkoutUrl + totalQuantity + buyerIdentity { + countryCode + customer { + id + email + firstName + lastName + displayName + } + email + phone + } + lines(first: $numCartLines) { + nodes { + ...CartLine + } + nodes { + ...CartLineComponent + } + } + cost { + subtotalAmount { + ...Money + } + totalAmount { + ...Money + } + totalDutyAmount { + ...Money + } + totalTaxAmount { + ...Money + } + } + note + attributes { + key + value + } + discountCodes { + code + applicable + } + } +` as const; + +const MENU_FRAGMENT = `#graphql + fragment MenuItem on MenuItem { + id + resourceId + tags + title + type + url + } + fragment ChildMenuItem on MenuItem { + ...MenuItem + } + fragment ParentMenuItem on MenuItem { + ...MenuItem + items { + ...ChildMenuItem + } + } + fragment Menu on Menu { + id + items { + ...ParentMenuItem + } + } +` as const; + +export const HEADER_QUERY = `#graphql + fragment Shop on Shop { + id + name + description + primaryDomain { + url + } + brand { + logo { + image { + url + } + } + } + } + query Header( + $country: CountryCode + $headerMenuHandle: String! + $language: LanguageCode + ) @inContext(language: $language, country: $country) { + shop { + ...Shop + } + menu(handle: $headerMenuHandle) { + ...Menu + } + } + ${MENU_FRAGMENT} +` as const; + +export const FOOTER_QUERY = `#graphql + query Footer( + $country: CountryCode + $footerMenuHandle: String! + $language: LanguageCode + ) @inContext(language: $language, country: $country) { + menu(handle: $footerMenuHandle) { + ...Menu + } + } + ${MENU_FRAGMENT} +` as const; diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/search.ts b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/search.ts new file mode 100644 index 000000000..f4b45631f --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/search.ts @@ -0,0 +1,79 @@ +import type { + PredictiveSearchQuery, + RegularSearchQuery, +} from 'storefrontapi.generated'; + +type ResultWithItems = { + type: Type; + term: string; + error?: string; + result: {total: number; items: Items}; +}; + +export type RegularSearchReturn = ResultWithItems< + 'regular', + RegularSearchQuery +>; +export type PredictiveSearchReturn = ResultWithItems< + 'predictive', + NonNullable +>; + +/** + * Returns the empty state of a predictive search result to reset the search state. + */ +export function getEmptyPredictiveSearchResult(): PredictiveSearchReturn['result'] { + return { + total: 0, + items: { + articles: [], + collections: [], + products: [], + pages: [], + queries: [], + }, + }; +} + +interface UrlWithTrackingParams { + /** The base URL to which the tracking parameters will be appended. */ + baseUrl: string; + /** The trackingParams returned by the Storefront API. */ + trackingParams?: string | null; + /** Any additional query parameters to be appended to the URL. */ + params?: Record; + /** The search term to be appended to the URL. */ + term: string; +} + +/** + * A utility function that appends tracking parameters to a URL. Tracking parameters are + * used internally by shopify to enhance search results and admin dashboards. + * @example + * ```ts + * const url = 'www.example.com'; + * const trackingParams = 'utm_source=shopify&utm_medium=shopify_app&utm_campaign=storefront'; + * const params = { foo: 'bar' }; + * const term = 'search term'; + * const url = urlWithTrackingParams({ baseUrl, trackingParams, params, term }); + * console.log(url); + * // Output: 'https://www.example.com?foo=bar&q=search%20term&utm_source=shopify&utm_medium=shopify_app&utm_campaign=storefront' + * ``` + */ +export function urlWithTrackingParams({ + baseUrl, + trackingParams, + params: extraParams, + term, +}: UrlWithTrackingParams) { + let search = new URLSearchParams({ + ...extraParams, + q: encodeURIComponent(term), + }).toString(); + + if (trackingParams) { + search = `${search}&${trackingParams}`; + } + + return `${baseUrl}?${search}`; +} diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/session.ts b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/session.ts new file mode 100644 index 000000000..4321a00a2 --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/session.ts @@ -0,0 +1,72 @@ +import type {HydrogenSession} from '@shopify/hydrogen'; +import { + createCookieSessionStorage, + type SessionStorage, + type Session, +} from '@netlify/remix-runtime'; + +/** + * This is a custom session implementation for your Hydrogen shop. + * Feel free to customize it to your needs, add helper methods, or + * swap out the cookie-based implementation with something else! + */ +export class AppSession implements HydrogenSession { + public isPending = false; + + #sessionStorage; + #session; + + constructor(sessionStorage: SessionStorage, session: Session) { + this.#sessionStorage = sessionStorage; + this.#session = session; + } + + static async init(request: Request, secrets: string[]) { + const storage = createCookieSessionStorage({ + cookie: { + name: 'session', + httpOnly: true, + path: '/', + sameSite: 'lax', + secrets, + }, + }); + + const session = await storage + .getSession(request.headers.get('Cookie')) + .catch(() => storage.getSession()); + + return new this(storage, session); + } + + get has() { + return this.#session.has; + } + + get get() { + return this.#session.get; + } + + get flash() { + return this.#session.flash; + } + + get unset() { + this.isPending = true; + return this.#session.unset; + } + + get set() { + this.isPending = true; + return this.#session.set; + } + + destroy() { + return this.#sessionStorage.destroySession(this.#session); + } + + commit() { + this.isPending = false; + return this.#sessionStorage.commitSession(this.#session); + } +} diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/variants.ts b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/variants.ts new file mode 100644 index 000000000..ffea0a730 --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/lib/variants.ts @@ -0,0 +1,46 @@ +import {useLocation} from '@remix-run/react'; +import type {SelectedOption} from '@shopify/hydrogen/storefront-api-types'; +import {useMemo} from 'react'; + +export function useVariantUrl( + handle: string, + selectedOptions: SelectedOption[], +) { + const {pathname} = useLocation(); + + return useMemo(() => { + return getVariantUrl({ + handle, + pathname, + searchParams: new URLSearchParams(), + selectedOptions, + }); + }, [handle, selectedOptions, pathname]); +} + +export function getVariantUrl({ + handle, + pathname, + searchParams, + selectedOptions, +}: { + handle: string; + pathname: string; + searchParams: URLSearchParams; + selectedOptions: SelectedOption[]; +}) { + const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname); + const isLocalePathname = match && match.length > 0; + + const path = isLocalePathname + ? `${match![0]}products/${handle}` + : `/products/${handle}`; + + selectedOptions.forEach((option) => { + searchParams.set(option.name, option.value); + }); + + const searchString = searchParams.toString(); + + return path + (searchString ? '?' + searchParams.toString() : ''); +} diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/root.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/root.tsx new file mode 100644 index 000000000..b278876fb --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/root.tsx @@ -0,0 +1,183 @@ +import { useNonce, getShopAnalytics, Analytics } from '@shopify/hydrogen' +import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime' +import { + Links, + Meta, + Outlet, + Scripts, + useRouteError, + useRouteLoaderData, + ScrollRestoration, + isRouteErrorResponse, + type ShouldRevalidateFunction, +} from '@remix-run/react' +import favicon from '~/assets/favicon.svg' +import resetStyles from '~/styles/reset.css?url' +import appStyles from '~/styles/app.css?url' +import { PageLayout } from '~/components/PageLayout' +import { FOOTER_QUERY, HEADER_QUERY } from '~/lib/fragments' + +export type RootLoader = typeof loader + +/** + * This is important to avoid re-fetching root queries on sub-navigations + */ +export const shouldRevalidate: ShouldRevalidateFunction = ({ + formMethod, + currentUrl, + nextUrl, + defaultShouldRevalidate, +}) => { + // revalidate when a mutation is performed e.g add to cart, login... + if (formMethod && formMethod !== 'GET') return true + + // revalidate when manually revalidating via useRevalidator + if (currentUrl.toString() === nextUrl.toString()) return true + + return defaultShouldRevalidate +} + +export function links() { + return [ + { rel: 'stylesheet', href: resetStyles }, + { rel: 'stylesheet', href: appStyles }, + { + rel: 'preconnect', + href: 'https://cdn.shopify.com', + }, + { + rel: 'preconnect', + href: 'https://shop.app', + }, + { rel: 'icon', type: 'image/svg+xml', href: favicon }, + ] +} + +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) + + const { storefront, env } = args.context + + return defer({ + ...deferredData, + ...criticalData, + publicStoreDomain: env.PUBLIC_STORE_DOMAIN, + shop: getShopAnalytics({ + storefront, + publicStorefrontId: env.PUBLIC_STOREFRONT_ID, + }), + consent: { + checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, + storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, + }, + }) +} + +/** + * 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 }: LoaderFunctionArgs) { + const { storefront } = context + + const [header] = await Promise.all([ + storefront.query(HEADER_QUERY, { + cache: storefront.CacheLong(), + variables: { + headerMenuHandle: 'main-menu', // Adjust to your header menu handle + }, + }), + // Add other queries here, so that they are loaded in parallel + ]) + + return { + header, + } +} + +/** + * 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) { + const { storefront, cart } = context + + // defer the footer query (below the fold) + const footer = storefront + .query(FOOTER_QUERY, { + cache: storefront.CacheLong(), + variables: { + footerMenuHandle: 'footer', // Adjust to your footer menu handle + }, + }) + .catch((error) => { + // Log query errors, but don't throw them so the page can still render + console.error(error) + return null + }) + return { + cart: cart.get(), + footer, + } +} + +export function Layout({ children }: { children?: React.ReactNode }) { + const nonce = useNonce() + const data = useRouteLoaderData('root') + + return ( + + + + + + + + + {data ? ( + + {children} + + ) : ( + children + )} + + + + + ) +} + +export default function App() { + return +} + +export function ErrorBoundary() { + const error = useRouteError() + let errorMessage = 'Unknown error' + let errorStatus = 500 + + if (isRouteErrorResponse(error)) { + errorMessage = error?.data?.message ?? error.data + errorStatus = error.status + } else if (error instanceof Error) { + errorMessage = error.message + } + + return ( +
    +

    Oops

    +

    {errorStatus}

    + {errorMessage && ( +
    +
    {errorMessage}
    +
    + )} +
    + ) +} diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/$.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/$.tsx new file mode 100644 index 000000000..87a8b4b99 --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/$.tsx @@ -0,0 +1,11 @@ +import type { LoaderFunctionArgs } from '@netlify/remix-runtime' + +export async function loader({ request }: LoaderFunctionArgs) { + throw new Response(`${new URL(request.url).pathname} not found`, { + status: 404, + }) +} + +export default function CatchAllPage() { + return null +} diff --git a/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/_index.tsx b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/_index.tsx new file mode 100644 index 000000000..68f5ce1b7 --- /dev/null +++ b/tests/e2e/fixtures/hydrogen-vite-site-no-entrypoint/app/routes/_index.tsx @@ -0,0 +1,158 @@ +import { defer, type LoaderFunctionArgs } from '@netlify/remix-runtime' +import { Await, useLoaderData, Link, type MetaFunction } from '@remix-run/react' +import { Suspense } from 'react' +import { Image, Money } from '@shopify/hydrogen' +import type { FeaturedCollectionFragment, RecommendedProductsQuery } from 'storefrontapi.generated' + +export const meta: MetaFunction = () => { + return [{ title: 'Hydrogen | Home' }] +} + +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 }: LoaderFunctionArgs) { + const [{ collections }] = await Promise.all([ + context.storefront.query(FEATURED_COLLECTION_QUERY), + // Add other queries here, so that they are loaded in parallel + ]) + + return { + featuredCollection: collections.nodes[0], + } +} + +/** + * 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) { + const recommendedProducts = context.storefront.query(RECOMMENDED_PRODUCTS_QUERY).catch((error) => { + // Log query errors, but don't throw them so the page can still render + console.error(error) + return null + }) + + return { + recommendedProducts, + } +} + +export default function Homepage() { + const data = useLoaderData() + return ( +
    + + +
    + ) +} + +function FeaturedCollection({ collection }: { collection: FeaturedCollectionFragment }) { + if (!collection) return null + const image = collection?.image + return ( + + {image && ( +
    + +
    + )} +

    {collection.title}

    + + ) +} + +function RecommendedProducts({ products }: { products: Promise }) { + return ( +
    +

    Recommended Products

    + Loading...
    }> + + {(response) => ( +
    + {response + ? response.products.nodes.map((product) => ( + + +

    {product.title}

    + + + + + )) + : null} +
    + )} +
    + +
    +
    + ) +} + +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.image.altText +
    + )} +

    {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.featuredImage.altText + )} +

    {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.image.altText + )} +
    {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.featuredImage.altText + )} +

    {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 ( +
    +
    +

    {page.title}

    +
    +
    +
    + ) +} + +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 }) => ( + <> + +   + + + )} + + {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) => ( + <> + + + + )} + + ) +} 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 ( +
    + +
    +
    {children}
    + + + ) +} + +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 && {title}} + +
    + { + 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 ( + + + + ) +} + +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 ( +
    +
    + ) +} + +function CartEmpty({ hidden = false }: { hidden: boolean; layout?: CartMainProps['layout'] }) { + const { close } = useAside() + return ( + + ) +} 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 */} + + + {/* Show an input to apply a discount */} + +
    + +   + +
    +
    +
    + ) +} + +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 ( + + ) +} + +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 ( +
    + + {shop.name} + + + +
    + ) +} + +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 ( + + ) +} + +function HeaderCtas({ cart }: Pick) { + return ( + + ) +} + +function HeaderMenuMobileToggle() { + const { open } = useAside() + return ( + + ) +} + +function SearchToggle() { + const { open } = useAside() + return ( + + ) +} + +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}
    +