Skip to content

Commit 8d9fbfc

Browse files
committed
feat(codegen): warn when hydrogen schema version differs
Signed-off-by: Frederik Bußmann <frederik@bussmann.io>
1 parent 832f82d commit 8d9fbfc

11 files changed

Lines changed: 155 additions & 51 deletions

File tree

‎docs/content/2.essentials/4.customer-account.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ npm install @shopify/hydrogen
3232
## Setup
3333

3434
For the type generation to work correctly, make sure to install the `@shopify/hydrogen` package for the appropriate version of the API
35-
you are using (e.g. `@shopify/hydrogen@2026.4.0` for API version 2026-04).
35+
you are using (e.g. `@shopify/hydrogen@2026.4.0` for API version 2026-04). The module warns during type generation when two versions differ.
3636

3737
The customer account session is stored in an encrypted cookie.
3838
In development, a session password is generated automatically and written to your `.env` file. In production,

‎pnpm-lock.yaml‎

Lines changed: 14 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/setup/graphql-config.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import type { Nuxt } from '@nuxt/schema'
22

33
import type { ShopifyConfig } from '../types'
44

5-
import { relative, sep } from 'node:path'
5+
import { relative } from 'node:path'
66

77
import { addTemplate } from '@nuxt/kit'
88

99
import { ShopifyClientType } from '../schemas'
1010
import { getConfiguredClients } from '../utils/clients'
1111
import { getIntrospectionFilename } from '../utils/templates'
1212
import { useLogger } from '../utils/log'
13+
import { toPosix } from '../utils/codegen'
1314

1415
const PROJECT_ORDER = [
1516
ShopifyClientType.Admin,
@@ -19,8 +20,6 @@ const PROJECT_ORDER = [
1920

2021
const DIR_IGNORES = ['node_modules', 'dist', '.nuxt', '.output']
2122

22-
const toPosix = (path: string) => path.split(sep).join('/')
23-
2423
interface GraphqlProject {
2524
schema: string
2625
documents: string[]

‎src/setup/graphql-transform.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { createGraphqlTransformPlugin } from '../utils/graphql/transform'
1515
import { getConfiguredClients } from '../utils/clients'
1616
import { scanFragments } from '../utils/graphql/registry'
1717
import { useLogger } from '../utils/log'
18+
import { toPosix } from '../utils/codegen'
1819

1920
const ROUTING_ORDER = [
2021
ShopifyClientType.Admin,
@@ -24,8 +25,6 @@ const ROUTING_ORDER = [
2425

2526
const AUTO_IMPORT_CHANGING_EVENTS = ['add', 'unlink'] as const
2627

27-
const toPosix = (path: string) => path.split(sep).join('/')
28-
2928
const within = (dirs: string[], path: string) =>
3029
dirs.some(dir => path === dir || path.startsWith(dir + sep))
3130

‎src/setup/vite.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ export default function setupVite(nuxt: Nuxt, config: ShopifyConfig) {
1919
deps.push('@shopify/graphql-client')
2020
}
2121

22-
if (config.clients?.customerAccount) {
23-
deps.push('@shopify/hydrogen')
24-
}
25-
2622
if (config.analytics) {
2723
deps.push('@shopify/hydrogen-react')
2824
}

‎src/utils/codegen.ts‎

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import type { ShopifyConfig } from '../types'
55

66
import { existsSync, statSync } from 'node:fs'
77
import { mkdir, stat, writeFile } from 'node:fs/promises'
8-
import { basename, dirname } from 'node:path'
9-
import { fileURLToPath } from 'node:url'
8+
import { basename, dirname, sep } from 'node:path'
109
import { generate } from '@graphql-codegen/cli'
1110
import { preset, pluckConfig } from '@shopify/graphql-codegen'
1211
import { LogLevels } from 'consola'
@@ -16,7 +15,7 @@ import { joinURL } from 'ufo'
1615

1716
import { ShopifyClientType } from '../schemas'
1817
import { useLogger } from './log'
19-
import { isInstalled } from './install'
18+
import { getInstalledVersion, resolveInstalledPath } from './install'
2019
import { getAdminAccessToken } from '../runtime/utils/clients/admin/auth'
2120
import {
2221
ADMIN_TOKEN_HEADER,
@@ -25,12 +24,11 @@ import {
2524
PUBLIC_TOKEN_HEADER,
2625
} from '../runtime/utils/clients/transport'
2726

28-
const HYDROGEN_STOREFRONT_SCHEMA = '@shopify/hydrogen/storefront.schema.json'
29-
const HYDROGEN_CUSTOMER_ACCOUNT_SCHEMA = '@shopify/hydrogen/customer-account.schema.json'
27+
const HYDROGEN = '@shopify/hydrogen'
3028

3129
const HYDROGEN_SCHEMAS: Partial<Record<ShopifyClientType, string>> = {
32-
[ShopifyClientType.Storefront]: HYDROGEN_STOREFRONT_SCHEMA,
33-
[ShopifyClientType.CustomerAccount]: HYDROGEN_CUSTOMER_ACCOUNT_SCHEMA,
30+
[ShopifyClientType.Storefront]: `${HYDROGEN}/storefront.schema.json`,
31+
[ShopifyClientType.CustomerAccount]: `${HYDROGEN}/customer-account.schema.json`,
3432
}
3533

3634
const INTROSPECTION_ATTEMPTS = 3
@@ -61,18 +59,43 @@ type InterfaceExtensionsParams = {
6159
mutationType: string
6260
}
6361

64-
function resolveSchemaPath(schemaId: string) {
65-
return fileURLToPath(import.meta.resolve(schemaId))
66-
}
62+
export const toPosix = (path: string) => path.split(sep).join('/')
6763

6864
function getHydrogenSchema(clientType: ShopifyClientType) {
6965
const schemaId = HYDROGEN_SCHEMAS[clientType]
66+
const schemaPath = schemaId && resolveInstalledPath(schemaId)
7067

71-
if (!schemaId || !isInstalled(schemaId)) {
72-
return undefined
73-
}
68+
return schemaPath ? [schemaPath] : undefined
69+
}
70+
71+
function isHydrogenSchema(schema: Types.ConfiguredOutput['schema'], clientType: ShopifyClientType) {
72+
const [hydrogenSchema] = getHydrogenSchema(clientType) ?? []
73+
74+
return !!hydrogenSchema && [schema].flat().includes(hydrogenSchema)
75+
}
76+
77+
async function getHydrogenSchemaApiVersion() {
78+
const version = await getInstalledVersion(HYDROGEN)
79+
const [year, month] = version?.split('.').map(Number) ?? []
80+
81+
if (!year || !month) return undefined
7482

75-
return [resolveSchemaPath(schemaId)]
83+
const apiReleaseMonth = month - ((month - 1) % 3)
84+
85+
return `${year}-${String(apiReleaseMonth).padStart(2, '0')}`
86+
}
87+
88+
async function warnOnHydrogenSchemaMismatch({ clientType, clientConfig }: ShopifyTemplateOptions) {
89+
const schemaApiVersion = await getHydrogenSchemaApiVersion()
90+
const apiVersion = clientConfig?.apiVersion
91+
92+
if (!schemaApiVersion || !apiVersion || schemaApiVersion === apiVersion) return
93+
94+
useLogger().warn(
95+
`The ${kebabCase(clientType)} types are generated from the \`${HYDROGEN}\` schema for API version \`${schemaApiVersion}\`, `
96+
+ `but the client uses \`${apiVersion}\`. Set \`clients.${clientType}.apiVersion\` to \`${schemaApiVersion}\`, `
97+
+ `or install the matching \`${HYDROGEN}\` release`,
98+
)
7699
}
77100

78101
function isRemoteSchema(schema: Types.ConfiguredOutput['schema']) {
@@ -276,7 +299,7 @@ async function getIntrospectionSource(options: ShopifyTemplateOptions) {
276299
}
277300
}
278301
else if (clientType === ShopifyClientType.CustomerAccount) {
279-
return [resolveSchemaPath(HYDROGEN_CUSTOMER_ACCOUNT_SCHEMA)]
302+
return getHydrogenSchema(clientType)
280303
}
281304
else if (clientType === ShopifyClientType.Admin) {
282305
const adminConfig = clientConfig as NonNullable<ShopifyConfig['clients']['admin']>
@@ -334,6 +357,10 @@ async function generateIntrospection(nuxt: Nuxt, options: ShopifyTemplateOptions
334357
config: generatorConfig,
335358
})
336359

360+
if (isHydrogenSchema(generatorConfig.schema, options.clientType)) {
361+
await warnOnHydrogenSchemaMismatch(options)
362+
}
363+
337364
const contents = await runIntrospection(nuxt, options, generatorConfig, config => ({
338365
overwrite: true,
339366
ignoreNoDocuments: true,

‎src/utils/graphql/registry.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { GraphqlDefinition } from '../../runtime/utils/graphql/scanner'
22

33
import { readFile, readdir } from 'node:fs/promises'
4-
import { join, relative, sep } from 'node:path'
4+
import { join, relative } from 'node:path'
55

66
import { collectSpreads, scanDefinitions } from '../../runtime/utils/graphql/scanner'
77
import { useLogger } from '../log'
88
import { findGraphqlLiterals } from './literals'
9+
import { toPosix } from '../codegen'
910

1011
const SCANNABLE = /\.(?:ts|js|mjs|cjs|mts|cts|vue|gql|graphql)$/
1112
const SCHEMA_LANGUAGE = /\.(?:gql|graphql)$/
@@ -35,8 +36,6 @@ function createRegistry(): FragmentRegistry {
3536
return { fragments: new Map(), provides: new Map() }
3637
}
3738

38-
const toPosix = (path: string) => path.split(sep).join('/')
39-
4039
const hasUnresolvedInterpolation = (text: string) => text.includes('${')
4140

4241
async function listFiles(dir: string): Promise<string[]> {

‎src/utils/install.ts‎

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import { directoryToURL, resolveModule } from '@nuxt/kit'
1+
import { directoryToURL } from '@nuxt/kit'
2+
import { resolveModulePath } from 'exsolve'
3+
import { readPackageJSON } from 'pkg-types'
4+
5+
export function resolveInstalledPath(id: string): string | undefined {
6+
return resolveModulePath(id, { from: import.meta.url, try: true })
7+
}
28

39
export function isInstalled(id: string): boolean {
4-
try {
5-
import.meta.resolve(id)
6-
return true
7-
}
8-
catch {
9-
return false
10-
}
10+
return !!resolveInstalledPath(id)
1111
}
1212

1313
export function isResolvableFrom(id: string, directory: string): boolean {
14-
try {
15-
resolveModule(id, { url: directoryToURL(directory) })
16-
return true
17-
}
18-
catch {
19-
return false
20-
}
14+
return !!resolveModulePath(id, { from: directoryToURL(directory), try: true })
15+
}
16+
17+
export async function getInstalledVersion(id: string): Promise<string | undefined> {
18+
const packageJson = await readPackageJSON(id, { from: import.meta.url }).catch(() => undefined)
19+
20+
return packageJson?.version
2121
}

‎template/app/composables/cart.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const useCart = () => {
44
const { t } = useI18n()
55

66
const analytics = useShopifyAnalytics()
7+
const { params: localization } = useLocalization()
78

89
const cart = useState<CartFieldsFragment | undefined>('shopify-cart', () => undefined)
910
const loading = useState('shopify-cart-loading', () => ref(false))
@@ -77,6 +78,7 @@ export const useCart = () => {
7778
quantity,
7879
},
7980
],
81+
...localization.value,
8082
}),
8183
})).then(({ data }) => {
8284
id.value = data?.cartCreate?.cart?.id
@@ -101,6 +103,7 @@ export const useCart = () => {
101103
`, {
102104
variables: cartGetInputSchema.parse({
103105
id: id.value,
106+
...localization.value,
104107
}),
105108
})).then(({ data }) => {
106109
if (!data?.cart) id.value = undefined
@@ -137,6 +140,7 @@ export const useCart = () => {
137140
quantity,
138141
},
139142
],
143+
...localization.value,
140144
}),
141145
})).then(({ data }) =>
142146
notifyAdded(variantId, data?.cartLinesAdd?.cart ?? undefined),
@@ -168,6 +172,7 @@ export const useCart = () => {
168172
quantity,
169173
},
170174
],
175+
...localization.value,
171176
}),
172177
})).then(({ data }) => {
173178
setCart(data?.cartLinesUpdate?.cart ?? undefined)
@@ -203,6 +208,7 @@ export const useCart = () => {
203208
variables: cartRemoveInputSchema.parse({
204209
cartId: id.value,
205210
lineIds: [variantId],
211+
...localization.value,
206212
}),
207213
})).then(({ data }) => {
208214
setCart(data?.cartLinesRemove?.cart ?? undefined)

0 commit comments

Comments
 (0)