Skip to content

Commit 5fd4777

Browse files
committed
feat: refactor config resolution and requirements setup
Signed-off-by: Frederik Bußmann <frederik@bussmann.io>
1 parent 4c8fdb8 commit 5fd4777

12 files changed

Lines changed: 171 additions & 225 deletions

src/module.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import setupCodegen from './setup/codegen'
1616
import setupGraphqlConfig from './setup/graphql-config'
1717
import setupImports from './setup/imports'
1818
import setupProxy from './setup/proxy'
19-
import setupRequirements from './setup/requirements'
2019
import setupSandbox from './setup/sandbox'
2120
import setupVite from './setup/vite'
2221
import setupWebhooks from './setup/webhooks'
@@ -47,20 +46,18 @@ export default defineNuxtModule<ModuleOptions>({
4746

4847
const logger = initLogger(rawConfig?.logger)
4948

50-
const moduleOptions = configSchema.safeParse(rawConfig)
51-
const publicModuleOptions = publicConfigSchema.safeParse(rawConfig)
49+
const moduleOptions = await configSchema.safeParseAsync({ ...rawConfig, _nuxt: nuxt })
5250

53-
if (moduleOptions.success && publicModuleOptions.success) {
51+
if (moduleOptions.success) {
5452
logger.start('Starting setup')
5553

5654
const config = moduleOptions.data
57-
const publicConfig = publicModuleOptions.data
5855

5956
logger.debug(`Configured clients: ${getConfiguredClients(config).join(', ') || 'none'}`)
6057

6158
await nuxt.callHook('shopify:config', { nuxt, config })
6259

63-
setupRequirements(config, publicConfig)
60+
const publicConfig = publicConfigSchema.parse(config)
6461

6562
Object.assign(nuxt.options.runtimeConfig, defu({
6663
_shopify: config,
@@ -70,8 +67,7 @@ export default defineNuxtModule<ModuleOptions>({
7067
},
7168
}, nuxt.options.runtimeConfig))
7269

73-
await setupClients(nuxt, config, resolver)
74-
70+
setupClients(nuxt, config, resolver)
7571
setupCodegen(nuxt, config)
7672
setupAnalytics(config, resolver)
7773
setupImports(nuxt, config, resolver)
@@ -88,12 +84,7 @@ export default defineNuxtModule<ModuleOptions>({
8884
logger.success('Finished setup')
8985
}
9086
else if (Object.keys(rawConfig ?? {}).length) {
91-
const issues = [...new Set(
92-
[moduleOptions.error, publicModuleOptions.error]
93-
.flatMap(error => error ? [z.prettifyError(error)] : []),
94-
)].join('\n')
95-
96-
logger.error(`Skipping setup: invalid module configuration\n${issues}`)
87+
logger.error(`Skipping setup: invalid module configuration\n${z.prettifyError(moduleOptions.error)}`)
9788
logger.info('See the module configuration reference: https://shopify.nuxtjs.org/essentials/configuration')
9889
}
9990
else {

src/schemas/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ const adminClient = adminClientSchema
223223
message: 'Either an access token or both client ID and client secret must be provided for the admin client',
224224
})
225225

226-
export const configSchema = z.object({
226+
export const configObjectSchema = z.object({
227227
name: z.string({ error: 'Shop name is required' }).min(1, { error: 'Shop name cannot be empty' }),
228228

229229
clients: z.object({
@@ -252,7 +252,7 @@ export const configSchema = z.object({
252252
logger: z.any().transform(v => v as Partial<ConsolaOptions>).optional(),
253253
})
254254

255-
export const publicConfigSchema = configSchema.omit({ clients: true, fragments: true, webhooks: true }).extend({
255+
export const publicConfigSchema = configObjectSchema.omit({ clients: true, fragments: true, webhooks: true }).extend({
256256
clients: z.object({
257257
[ShopifyClientType.Storefront]: storefrontClientSchema.omit({
258258
privateAccessToken: true,
@@ -282,5 +282,5 @@ export const publicConfigSchema = configSchema.omit({ clients: true, fragments:
282282
}).optional(),
283283
}).optional().default({}),
284284

285-
errors: configSchema.shape.errors,
285+
errors: configObjectSchema.shape.errors,
286286
})

src/schemas/index.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
1-
import type { z } from 'zod'
1+
import type { Nuxt } from '@nuxt/schema'
2+
3+
import { z } from 'zod'
24

35
import {
46
ShopifyClientType,
5-
configSchema,
7+
configObjectSchema,
68
publicConfigSchema,
79
} from './config'
10+
import { resolveConfig } from './resolve'
11+
12+
export const configSchema = configObjectSchema
13+
.extend({ _nuxt: z.any().optional().transform(v => v as Nuxt | undefined) })
14+
.transform(({ _nuxt, ...config }) => resolveConfig(config, _nuxt))
815

9-
export type ModuleOptions = z.input<typeof configSchema>
16+
export type ModuleOptions = z.input<typeof configObjectSchema>
1017
export type PublicModuleOptions = z.input<typeof publicConfigSchema>
1118

12-
export type ShopifyConfig = z.output<typeof configSchema>
19+
export type ShopifyConfig = z.output<typeof configObjectSchema>
1320
export type PublicShopifyConfig = z.output<typeof publicConfigSchema>
1421

1522
export {
1623
ShopifyClientType,
1724

18-
configSchema,
25+
configObjectSchema,
1926
publicConfigSchema,
2027
}

src/schemas/resolve.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import type { Nuxt } from '@nuxt/schema'
2+
import type { z } from 'zod'
3+
4+
import type { configObjectSchema } from './config'
5+
6+
import { ShopifyClientType } from './config'
7+
import { createStoreDomain } from '../runtime/utils/clients/transport'
8+
import { isInstalled } from '../utils/install'
9+
import { useLogger } from '../utils/log'
10+
import { SESSION_PASSWORD_ENV, generateSessionPassword, persistSessionPassword } from '../utils/session'
11+
12+
type ShopifyConfig = z.output<typeof configObjectSchema>
13+
14+
const HYDROGEN = '@shopify/hydrogen'
15+
const HYDROGEN_REACT = '@shopify/hydrogen-react'
16+
17+
const proxyableClients = [ShopifyClientType.Storefront, ShopifyClientType.CustomerAccount] as const
18+
19+
function resolveRequirements(config: ShopifyConfig) {
20+
const logger = useLogger()
21+
22+
if (config.clients[ShopifyClientType.CustomerAccount] && !isInstalled(HYDROGEN)) {
23+
logger.error(`The customer account client is configured but \`${HYDROGEN}\` is not installed. Install it (e.g. \`npm i ${HYDROGEN}\`) or remove \`shopify.clients.customerAccount\`. Disabling customer account client.`)
24+
25+
config.clients[ShopifyClientType.CustomerAccount] = undefined
26+
}
27+
28+
if (config.analytics && !isInstalled(HYDROGEN_REACT)) {
29+
logger.error(`Analytics is enabled but \`${HYDROGEN_REACT}\` is not installed. Install it (e.g. \`npm i ${HYDROGEN_REACT}\`) or disable \`shopify.analytics\`. Disabling analytics.`)
30+
31+
config.analytics = false
32+
}
33+
34+
if (
35+
config.analytics
36+
&& !config.clients[ShopifyClientType.Storefront]?.publicAccessToken
37+
&& !config.analytics.consent?.storefrontAccessToken
38+
) {
39+
logger.error('Analytics is enabled but no public storefront access token is set. Set `clients.storefront.publicAccessToken` or `analytics.consent.storefrontAccessToken`. Disabling analytics.')
40+
41+
config.analytics = false
42+
}
43+
}
44+
45+
function resolveProxies(config: ShopifyConfig, nuxt: Nuxt) {
46+
if (nuxt.options.ssr && !(nuxt.options as { _generate?: boolean })._generate) return
47+
48+
const logger = useLogger()
49+
50+
for (const clientType of proxyableClients) {
51+
const client = config.clients[clientType]
52+
53+
if (!client?.proxy) continue
54+
55+
logger.info(`Disabling the ${clientType} proxy: server-side request proxying requires SSR. Requests are sent to Shopify directly.`)
56+
57+
client.proxy = false
58+
}
59+
}
60+
61+
async function resolveCustomerAccountApiUrl(config: ShopifyConfig) {
62+
const customerAccount = config.clients[ShopifyClientType.CustomerAccount]
63+
64+
if (!customerAccount) return
65+
66+
const logger = useLogger()
67+
68+
if (customerAccount.apiUrl) {
69+
logger.debug(`Using the configured customer account API URL: ${customerAccount.apiUrl}`)
70+
71+
return
72+
}
73+
74+
const wellKnownURL = createStoreDomain(config.name) + '/.well-known/customer-account-api'
75+
76+
const apiUrl = await fetch(wellKnownURL)
77+
.then(async res => (await res.json() as { graphql_api: string }).graphql_api)
78+
.catch(() => undefined)
79+
80+
if (apiUrl) {
81+
logger.debug(`Resolved customer account API URL: ${apiUrl}`)
82+
}
83+
else {
84+
logger.warn(
85+
`Could not resolve the customer account API URL from \`${wellKnownURL}\` - `
86+
+ 'customer account requests will fail (is the Customer Account API enabled for your store?, '
87+
+ 'or set `clients.customerAccount.apiUrl` explicitly)',
88+
)
89+
}
90+
91+
customerAccount.apiUrl = apiUrl
92+
}
93+
94+
async function resolveSessionPassword(config: ShopifyConfig, nuxt: Nuxt) {
95+
const session = config.clients[ShopifyClientType.CustomerAccount]?.session
96+
97+
if (!session || session.password) return
98+
99+
const logger = useLogger()
100+
const envPassword = process.env[SESSION_PASSWORD_ENV]
101+
102+
if (envPassword) {
103+
session.password = envPassword
104+
105+
return
106+
}
107+
108+
if (!nuxt.options.dev) {
109+
logger.warn(`No customer account session password set - customer account sessions will fail until the \`${SESSION_PASSWORD_ENV}\` environment variable is set`)
110+
111+
return
112+
}
113+
114+
session.password = generateSessionPassword()
115+
116+
await persistSessionPassword(nuxt.options.rootDir, session.password)
117+
118+
logger.info('Generated a customer account session password in `.env`')
119+
}
120+
121+
export async function resolveConfig(config: ShopifyConfig, nuxt?: Nuxt): Promise<ShopifyConfig> {
122+
if (!nuxt) return config
123+
124+
resolveRequirements(config)
125+
resolveProxies(config, nuxt)
126+
127+
await resolveCustomerAccountApiUrl(config)
128+
await resolveSessionPassword(config, nuxt)
129+
130+
return config
131+
}

src/setup/clients.ts

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ import {
1515
registerClientAsyncImports,
1616
} from '../utils/clients'
1717
import { ShopifyClientType } from '../schemas'
18-
import { createStoreDomain } from '../runtime/utils/clients/transport'
19-
import { SESSION_PASSWORD_ENV, generateSessionPassword, persistSessionPassword } from '../utils/session'
2018

21-
export default async function setupClients(nuxt: Nuxt, config: ShopifyConfig, resolver: Resolver) {
19+
export default function setupClients(nuxt: Nuxt, config: ShopifyConfig, resolver: Resolver) {
2220
const logger = useLogger()
2321
const clients = getConfiguredClients(config)
2422

@@ -35,56 +33,6 @@ export default async function setupClients(nuxt: Nuxt, config: ShopifyConfig, re
3533
if (clientType === ShopifyClientType.CustomerAccount && config.clients[clientType]) {
3634
const customerAccount = config.clients[clientType]
3735

38-
if (customerAccount.apiUrl) {
39-
logger.debug(`Using the configured customer account API URL: ${customerAccount.apiUrl}`)
40-
}
41-
else if (
42-
nuxt.options.runtimeConfig._shopify?.clients.customerAccount
43-
&& nuxt.options.runtimeConfig.public._shopify?.clients.customerAccount
44-
) {
45-
const wellKnownURL = createStoreDomain(config.name) + '/.well-known/customer-account-api'
46-
47-
const apiUrl = await fetch(wellKnownURL)
48-
.then(async res => (await res.json() as { graphql_api: string }).graphql_api)
49-
.catch(() => undefined)
50-
51-
if (apiUrl) {
52-
logger.debug(`Resolved customer account API URL: ${apiUrl}`)
53-
}
54-
else {
55-
logger.warn(
56-
`Could not resolve the customer account API URL from \`${wellKnownURL}\` - `
57-
+ 'customer account requests will fail (is the Customer Account API enabled for your store?, '
58-
+ 'or set `clients.customerAccount.apiUrl` explicitly)',
59-
)
60-
}
61-
62-
nuxt.options.runtimeConfig._shopify.clients.customerAccount.apiUrl = apiUrl
63-
nuxt.options.runtimeConfig.public._shopify.clients.customerAccount.apiUrl = apiUrl
64-
}
65-
66-
const session = nuxt.options.runtimeConfig._shopify?.clients.customerAccount?.session
67-
68-
if (session && !session.password) {
69-
const envPassword = process.env[SESSION_PASSWORD_ENV]
70-
71-
if (envPassword) {
72-
session.password = envPassword
73-
}
74-
else if (nuxt.options.dev) {
75-
const password = generateSessionPassword()
76-
77-
session.password = password
78-
79-
await persistSessionPassword(nuxt.options.rootDir, password)
80-
81-
logger.info('Generated a customer account session password in `.env`')
82-
}
83-
else {
84-
logger.warn(`No customer account session password set - customer account sessions will fail until the \`${SESSION_PASSWORD_ENV}\` environment variable is set`)
85-
}
86-
}
87-
8836
addServerHandler({
8937
method: 'get',
9038
route: withLeadingSlash(customerAccount.loginURL),

src/setup/requirements.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/utils/proxy.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import type {
99
import { addServerHandler } from '@nuxt/kit'
1010
import { joinURL, withLeadingSlash } from 'ufo'
1111

12-
import { useLogger } from './log'
13-
import { kebabCase, upperFirst } from 'scule'
12+
import { kebabCase } from 'scule'
1413

1514
export function registerProxy(nuxt: Nuxt, config: ShopifyConfig, clientType: ShopifyClientType, resolver: Resolver): string | false {
1615
const clientConfig = config.clients[clientType]
@@ -21,14 +20,6 @@ export function registerProxy(nuxt: Nuxt, config: ShopifyConfig, clientType: Sho
2120

2221
if (!url) return false
2322

24-
if (!nuxt.options.ssr || (nuxt.options as { _generate?: boolean })._generate) {
25-
const logger = useLogger()
26-
27-
logger.info(`Skipping ${upperFirst(clientType)} proxy setup: server-side request proxying requires SSR`)
28-
29-
return false
30-
}
31-
3223
addServerHandler({
3324
handler: resolver.resolve(`./runtime/server/api/proxy/${kebabCase(clientType)}`),
3425
route: withLeadingSlash(url),

test/unit/analytics-config.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import { configSchema } from '../../src/schemas'
3+
import { configObjectSchema } from '../../src/schemas'
44

55
const base = {
66
name: 'shop',
77
clients: { storefront: { publicAccessToken: 'token' } },
88
}
99

1010
function parseAnalytics(analytics?: unknown) {
11-
return configSchema.parse(analytics === undefined ? base : { ...base, analytics }).analytics
11+
return configObjectSchema.parse(analytics === undefined ? base : { ...base, analytics }).analytics
1212
}
1313

1414
describe('analytics config', () => {

0 commit comments

Comments
 (0)