@akqa-denmark/shopify-theme-build 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # @akqa-denmark/shopify-theme-build
2
+
3
+ Internal build pipeline for AKQA Denmark Shopify themes. Handles schema generation, locale merging, section group scaffolding, and Vite config for multi-store repositories.
4
+
5
+ ---
6
+
7
+ ## What it does
8
+
9
+ Shopify themes require a specific set of JSON and Liquid files to be generated from TypeScript schema sources before a build or deploy. This package encapsulates that pipeline so it can be shared across projects without duplicating the build directory.
10
+
11
+ Specifically, it:
12
+
13
+ - Injects `{% schema %}` blocks into Liquid files for sections, blocks, and snippets
14
+ - Generates and maintains `theme/config/settings_schema.json` from TS config schemas
15
+ - Generates `theme/sections/{name}-group.json` files for section groups (preserving merchant-managed `sections` and `order` data)
16
+ - Extracts translatable strings from all schema types and merges them into `theme/locales/en.default.schema.json`
17
+ - Provides a Vite config factory (`createViteConfig`) that wires up vite-plugin-shopify, full-reload, tailwind, and a schema watcher plugin — all config-aware
18
+
19
+ ---
20
+
21
+ ## Requirements
22
+
23
+ - Node >= 22.0.0
24
+ - Vite >= 8.0.0
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @akqa-denmark/shopify-theme-build
32
+ ```
33
+
34
+ Peer dependencies (install as needed):
35
+
36
+ ```bash
37
+ npm install --save-dev vite vite-plugin-shopify vite-plugin-full-reload
38
+ npm install --save-dev @tailwindcss/vite # optional
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Setup
44
+
45
+ ### 1. Create `shopify-build.config.ts` at the repo root
46
+
47
+ ```typescript
48
+ import { defineConfig } from '@akqa-denmark/shopify-theme-build';
49
+
50
+ export default defineConfig({
51
+ stores: [
52
+ { slug: 'my-store', name: 'My Store' },
53
+ { slug: 'base', name: 'Base', foundation: true },
54
+ ],
55
+ defaultStore: 'my-store',
56
+ });
57
+ ```
58
+
59
+ `foundation: true` marks a store as non-deployable (excluded from `shopify-build manifest`).
60
+
61
+ Full config options:
62
+
63
+ ```typescript
64
+ interface BuildConfig {
65
+ stores: { slug: string; name: string; foundation?: boolean }[];
66
+ defaultStore: string;
67
+ storesDirectory?: string; // default: 'stores'
68
+ sharedDirectory?: string; // default: 'shared'
69
+ build?: {
70
+ parallel?: boolean; // default: true
71
+ };
72
+ vite?: {
73
+ port?: number; // default: 3000
74
+ bundledDev?: boolean; // default: false
75
+ };
76
+ }
77
+ ```
78
+
79
+ ### 2. Update `vite.config.ts`
80
+
81
+ ```typescript
82
+ import { createViteConfig } from '@akqa-denmark/shopify-theme-build/vite';
83
+
84
+ export default createViteConfig();
85
+ ```
86
+
87
+ Pass overrides to merge with the base config:
88
+
89
+ ```typescript
90
+ export default createViteConfig({
91
+ resolve: {
92
+ alias: [
93
+ { find: '@', replacement: new URL('./stores/my-store/src', import.meta.url).pathname },
94
+ { find: /^@shared\/(.*)/, replacement: new URL('./shared/$1', import.meta.url).pathname },
95
+ ],
96
+ },
97
+ });
98
+ ```
99
+
100
+ ### 3. Update `package.json` scripts
101
+
102
+ ```json
103
+ {
104
+ "scripts": {
105
+ "build:prepare": "shopify-build prepare",
106
+ "build:full": "shopify-build prepare && vite build"
107
+ }
108
+ }
109
+ ```
110
+
111
+ To target a specific store, use `--store` or set `CURRENT_STORE`:
112
+
113
+ ```bash
114
+ shopify-build prepare --store my-store
115
+ CURRENT_STORE=my-store shopify-build prepare
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Directory conventions
121
+
122
+ The pipeline expects this structure inside each store directory:
123
+
124
+ ```
125
+ stores/{slug}/
126
+ ├── src/
127
+ │ └── schemas/
128
+ │ ├── settings/ → locale data only
129
+ │ ├── configs/ → settings_schema.json entries
130
+ │ ├── sections/ → theme/sections/{name}.liquid schema injection
131
+ │ ├── blocks/ → theme/blocks/{name}.liquid schema injection
132
+ │ ├── section-blocks/ → theme/snippets/{name}.liquid schema injection
133
+ │ └── section-groups/ → theme/sections/{name}-group.json
134
+ └── theme/
135
+ ├── config/
136
+ │ └── settings_schema.json (generated)
137
+ ├── locales/
138
+ │ └── en.default.schema.json (generated)
139
+ ├── sections/
140
+ ├── blocks/
141
+ └── snippets/
142
+ ```
143
+
144
+ ---
145
+
146
+ ## CLI
147
+
148
+ ### `shopify-build prepare`
149
+
150
+ Runs the full schema pipeline for a store.
151
+
152
+ ```bash
153
+ shopify-build prepare
154
+ shopify-build prepare --store georg-jensen
155
+ shopify-build prepare --skip-schemas
156
+ shopify-build prepare --skip-locales
157
+ ```
158
+
159
+ ### `shopify-build manifest`
160
+
161
+ Outputs a JSON manifest of all deployable stores to stdout. Useful for CI matrix generation.
162
+
163
+ ```bash
164
+ shopify-build manifest
165
+ # {
166
+ # "stores": [
167
+ # { "slug": "my-store", "name": "My Store", "deployable": true },
168
+ # { "slug": "base", "name": "Base", "deployable": false }
169
+ # ]
170
+ # }
171
+ ```
172
+
173
+ ---
174
+
175
+ ## API
176
+
177
+ ```typescript
178
+ import { defineConfig, resolveConfig, resolveStore, getStorePaths, orchestrate } from '@akqa-denmark/shopify-theme-build';
179
+ import { createViteConfig } from '@akqa-denmark/shopify-theme-build/vite';
180
+ ```
181
+
182
+ | Export | Description |
183
+ |---|---|
184
+ | `defineConfig(config)` | Type-safe config helper |
185
+ | `resolveConfig(cwd?)` | Loads and validates `shopify-build.config.ts` |
186
+ | `resolveStore(config, explicit?)` | Resolves store from arg, `CURRENT_STORE` env, or `defaultStore` |
187
+ | `getStorePaths(config, store)` | Returns resolved path object for a store |
188
+ | `orchestrate(options)` | Runs the full pipeline programmatically |
189
+ | `createViteConfig(overrides?)` | Returns a Vite `UserConfig` |
190
+
191
+ ---
192
+
193
+ ## Schema file conventions
194
+
195
+ ### Sections and blocks
196
+
197
+ Export a default object or a function returning an object:
198
+
199
+ ```typescript
200
+ // stores/my-store/src/schemas/sections/hero.ts
201
+ export default {
202
+ name: 'Hero',
203
+ settings: [
204
+ { type: 'text', id: 'heading', label: 'Heading', defaultLabel: 'Heading' },
205
+ ],
206
+ };
207
+ ```
208
+
209
+ ### Configs
210
+
211
+ Export a named function in the form `{PascalName}Config`:
212
+
213
+ ```typescript
214
+ // stores/my-store/src/schemas/configs/brand.ts
215
+ export function BrandConfig() {
216
+ return {
217
+ name: 'Brand',
218
+ settings: [
219
+ { type: 'color', id: 'primary_color', label: 'Primary colour' },
220
+ ],
221
+ };
222
+ }
223
+ ```
224
+
225
+ `theme-info.ts` is handled separately — the pipeline injects `theme_version` from git tags and `theme_name` from the store's `name` in config.
226
+
227
+ ### Section groups
228
+
229
+ Export a default object with `type` and `name`. The `sections` and `order` fields are managed by the Shopify theme editor and are preserved from any existing file.
230
+
231
+ ```typescript
232
+ // stores/my-store/src/schemas/section-groups/header.ts
233
+ export default {
234
+ type: 'header',
235
+ name: 'Header group',
236
+ };
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Version resolution
242
+
243
+ `theme_version` in `settings_schema.json` is resolved at build time:
244
+
245
+ 1. `RELEASE_TAG` env var (CI deploy — e.g. `v1.18.0` → `1.18.0`)
246
+ 2. CI without `RELEASE_TAG` → latest git tag, no suffix
247
+ 3. Local dev → latest git tag + `-dev` suffix (e.g. `1.18.0-dev`)
248
+ 4. Fallback: `dev`
249
+
250
+ ---
251
+
252
+ ## Development
253
+
254
+ ```bash
255
+ npm run build # compile to dist/
256
+ npm run dev # watch mode
257
+ npm run type-check # tsc --noEmit
258
+ ```
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js').catch((err) => {
3
+ console.error('Build output not found. Run `npm run build` first.\n', err.message);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config/resolve.ts
4
+ import { existsSync } from "fs";
5
+ import { resolve, join } from "path";
6
+ var CONFIG_FILE_NAMES = [
7
+ "shopify-build.config.ts",
8
+ "shopify-build.config.js",
9
+ "shopify-build.config.mjs"
10
+ ];
11
+ async function resolveConfig(cwd) {
12
+ const rootDir = cwd || process.cwd();
13
+ const configPath = findConfigFile(rootDir);
14
+ if (!configPath) {
15
+ throw new Error(
16
+ `No config file found. Create one of: ${CONFIG_FILE_NAMES.join(", ")}`
17
+ );
18
+ }
19
+ const { createJiti } = await import("jiti");
20
+ const jiti = createJiti(rootDir);
21
+ const rawModule = await jiti.import(configPath);
22
+ const raw = rawModule.default || rawModule;
23
+ if (!raw.stores || raw.stores.length === 0) {
24
+ throw new Error("Config must define at least one store");
25
+ }
26
+ if (!raw.defaultStore) {
27
+ throw new Error("Config must define a defaultStore");
28
+ }
29
+ const defaultExists = raw.stores.some((s) => s.slug === raw.defaultStore);
30
+ if (!defaultExists) {
31
+ throw new Error(
32
+ `defaultStore "${raw.defaultStore}" not found in stores array`
33
+ );
34
+ }
35
+ return {
36
+ stores: raw.stores,
37
+ defaultStore: raw.defaultStore,
38
+ storesDirectory: raw.storesDirectory || "stores",
39
+ sharedDirectory: raw.sharedDirectory || "shared",
40
+ rootDir,
41
+ build: {
42
+ parallel: raw.build?.parallel ?? true
43
+ },
44
+ vite: {
45
+ port: raw.vite?.port ?? 3e3,
46
+ bundledDev: raw.vite?.bundledDev ?? false
47
+ }
48
+ };
49
+ }
50
+ function findConfigFile(rootDir) {
51
+ for (const name of CONFIG_FILE_NAMES) {
52
+ const fullPath = resolve(rootDir, name);
53
+ if (existsSync(fullPath)) return fullPath;
54
+ }
55
+ return null;
56
+ }
57
+ function resolveStore(config, explicit) {
58
+ const store = explicit || process.env.CURRENT_STORE || config.defaultStore;
59
+ const exists = config.stores.some((s) => s.slug === store);
60
+ if (!exists) {
61
+ const valid = config.stores.map((s) => s.slug).join(", ");
62
+ throw new Error(`Store "${store}" not found. Valid: ${valid}`);
63
+ }
64
+ return store;
65
+ }
66
+ function getStorePaths(config, store) {
67
+ const storeDir = join(config.rootDir, config.storesDirectory, store);
68
+ const themeDir = join(storeDir, "theme");
69
+ const srcDir = join(storeDir, "src");
70
+ const schemasDir = join(srcDir, "schemas");
71
+ const entrypointsDir = join(srcDir, "assets/scripts/entrypoints");
72
+ const localesDir = join(themeDir, "locales");
73
+ const configDir = join(themeDir, "config");
74
+ return { storeDir, themeDir, srcDir, schemasDir, entrypointsDir, localesDir, configDir };
75
+ }
76
+
77
+ export {
78
+ resolveConfig,
79
+ resolveStore,
80
+ getStorePaths
81
+ };