create-easel-theme 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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/bin/create-easel-theme.mjs +9 -0
  4. package/dist/cli.js +604 -0
  5. package/dist/templates/js/alpine/snippets/counter.liquid +9 -0
  6. package/dist/templates/js/alpine/src/main.js +19 -0
  7. package/dist/templates/js/base/_gitignore +5 -0
  8. package/dist/templates/js/base/assets/easel.svg +4 -0
  9. package/dist/templates/js/base/config/settings_data.json +12 -0
  10. package/dist/templates/js/base/config/settings_schema.json +27 -0
  11. package/dist/templates/js/base/layout/theme.liquid +19 -0
  12. package/dist/templates/js/base/locales/en.default.json +13 -0
  13. package/dist/templates/js/base/locales/en.default.schema.json +21 -0
  14. package/dist/templates/js/base/sections/hero.liquid +30 -0
  15. package/dist/templates/js/base/sections/main-404.liquid +11 -0
  16. package/dist/templates/js/base/snippets/counter.liquid +9 -0
  17. package/dist/templates/js/base/src/main.js +21 -0
  18. package/dist/templates/js/base/src/sections.js +44 -0
  19. package/dist/templates/js/base/src/style.css +61 -0
  20. package/dist/templates/js/base/templates/404.json +9 -0
  21. package/dist/templates/js/base/templates/index.json +9 -0
  22. package/dist/templates/js/react/src/main.jsx +23 -0
  23. package/dist/templates/js/vue/src/Counter.vue +18 -0
  24. package/dist/templates/js/vue/src/main.js +13 -0
  25. package/dist/templates/ts/alpine/snippets/counter.liquid +9 -0
  26. package/dist/templates/ts/alpine/src/main.ts +22 -0
  27. package/dist/templates/ts/base/_gitignore +5 -0
  28. package/dist/templates/ts/base/assets/easel.svg +4 -0
  29. package/dist/templates/ts/base/config/settings_data.json +12 -0
  30. package/dist/templates/ts/base/config/settings_schema.json +27 -0
  31. package/dist/templates/ts/base/layout/theme.liquid +19 -0
  32. package/dist/templates/ts/base/locales/en.default.json +13 -0
  33. package/dist/templates/ts/base/locales/en.default.schema.json +21 -0
  34. package/dist/templates/ts/base/sections/hero.liquid +30 -0
  35. package/dist/templates/ts/base/sections/main-404.liquid +11 -0
  36. package/dist/templates/ts/base/snippets/counter.liquid +9 -0
  37. package/dist/templates/ts/base/src/main.ts +29 -0
  38. package/dist/templates/ts/base/src/sections.ts +47 -0
  39. package/dist/templates/ts/base/src/style.css +61 -0
  40. package/dist/templates/ts/base/templates/404.json +9 -0
  41. package/dist/templates/ts/base/templates/index.json +9 -0
  42. package/dist/templates/ts/react/src/main.tsx +40 -0
  43. package/dist/templates/ts/vue/src/Counter.vue +18 -0
  44. package/dist/templates/ts/vue/src/main.ts +14 -0
  45. package/package.json +43 -0
package/dist/cli.js ADDED
@@ -0,0 +1,604 @@
1
+ // src/cli.ts
2
+ import * as p from "@clack/prompts";
3
+
4
+ // src/directory.ts
5
+ import { lstatSync, readdirSync } from "fs";
6
+ var preservedEntries = /* @__PURE__ */ new Set([".git", ".DS_Store"]);
7
+ function assertTargetDirectory(target) {
8
+ const metadata = lstatSync(target, { throwIfNoEntry: false });
9
+ if (!metadata) return;
10
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
11
+ throw new Error(`Target already exists and is not a regular directory: ${target}.`);
12
+ }
13
+ if (readdirSync(target).some((name) => !preservedEntries.has(name))) {
14
+ throw new Error(
15
+ `Target already exists and is not empty: ${target}. Use an empty directory (.git and .DS_Store are allowed). Existing files are never overwritten.`
16
+ );
17
+ }
18
+ return metadata;
19
+ }
20
+
21
+ // src/cli.ts
22
+ import { resolve as resolve3 } from "path";
23
+
24
+ // package.json
25
+ var version = "0.1.0";
26
+
27
+ // src/generate.ts
28
+ import {
29
+ cpSync,
30
+ mkdirSync as mkdirSync2,
31
+ mkdtempSync,
32
+ readFileSync as readFileSync2,
33
+ renameSync,
34
+ rmSync,
35
+ writeFileSync
36
+ } from "fs";
37
+ import { tmpdir } from "os";
38
+ import { join as join2, resolve as resolve2 } from "path";
39
+ import { fileURLToPath } from "url";
40
+
41
+ // src/options.ts
42
+ import { basename, resolve } from "path";
43
+ import { parseArgs } from "util";
44
+ var languages = ["ts", "js"];
45
+ var frameworks = ["none", "alpine", "react", "vue"];
46
+ var packageManagers = ["npm", "pnpm", "yarn", "bun"];
47
+ function parseArguments(args) {
48
+ const { values, positionals } = parseArgs({
49
+ args,
50
+ allowPositionals: true,
51
+ allowNegative: true,
52
+ strict: true,
53
+ options: {
54
+ help: { type: "boolean", short: "h" },
55
+ version: { type: "boolean", short: "v" },
56
+ yes: { type: "boolean", short: "y" },
57
+ name: { type: "string" },
58
+ language: { type: "string" },
59
+ framework: { type: "string" },
60
+ tailwind: { type: "boolean" },
61
+ install: { type: "boolean" },
62
+ "package-manager": { type: "string" },
63
+ interactive: { type: "boolean" }
64
+ }
65
+ });
66
+ if (positionals.length > 1) throw new Error("Provide only one target directory.");
67
+ if (values.language !== void 0) choice(values.language, languages, "--language");
68
+ if (values.framework !== void 0) choice(values.framework, frameworks, "--framework");
69
+ if (values["package-manager"] !== void 0) {
70
+ choice(values["package-manager"], packageManagers, "--package-manager");
71
+ }
72
+ if (values.name !== void 0) validateName(values.name);
73
+ return { ...values, target: positionals[0] };
74
+ }
75
+ function choice(value, options, label) {
76
+ const match = options.find((option) => option === value);
77
+ if (match === void 0)
78
+ throw new Error(`${label} must be one of: ${options.join(", ")}.`);
79
+ return match;
80
+ }
81
+ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
82
+ return packageManagers.find((manager) => userAgent?.startsWith(`${manager}/`)) ?? "npm";
83
+ }
84
+ function validateName(name) {
85
+ if (!/^[A-Za-z0-9][A-Za-z0-9 _-]{0,49}$/.test(name)) {
86
+ throw new Error(
87
+ "Theme name must be 1\u201350 characters: letters, numbers, spaces, underscores, or hyphens."
88
+ );
89
+ }
90
+ }
91
+ function packageNameFor(target) {
92
+ if (!target.trim() || /[\u0000-\u001f\u007f]/.test(resolve(target))) {
93
+ throw new Error("Provide a target directory without control characters.");
94
+ }
95
+ const name = basename(resolve(target));
96
+ if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)) {
97
+ throw new Error(
98
+ "The target uses a reserved Windows filename. Choose another directory."
99
+ );
100
+ }
101
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
102
+ if (!slug || slug.length > 214 || slug === "node-modules" || slug === "favicon-ico") {
103
+ throw new Error("The target directory must produce a valid npm package name.");
104
+ }
105
+ return slug;
106
+ }
107
+ function defaultName(target) {
108
+ return packageNameFor(target).split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ").slice(0, 50).trim();
109
+ }
110
+ function validateOptions(options) {
111
+ packageNameFor(options.target);
112
+ validateName(options.name);
113
+ choice(options.language, languages, "Language");
114
+ choice(options.framework, frameworks, "Framework");
115
+ if (typeof options.tailwind !== "boolean")
116
+ throw new Error("Tailwind must be a boolean.");
117
+ }
118
+
119
+ // src/write-project.ts
120
+ import {
121
+ constants,
122
+ copyFileSync,
123
+ lstatSync as lstatSync2,
124
+ mkdirSync,
125
+ readFileSync,
126
+ readdirSync as readdirSync2,
127
+ rmdirSync,
128
+ unlinkSync
129
+ } from "fs";
130
+ import { dirname, join } from "path";
131
+ function sameEntry(path, expected) {
132
+ const actual = lstatSync2(path, { throwIfNoEntry: false });
133
+ return Boolean(
134
+ actual && !actual.isSymbolicLink() && actual.dev === expected.dev && actual.ino === expected.ino
135
+ );
136
+ }
137
+ function writeProject(source, target) {
138
+ let root = assertTargetDirectory(target);
139
+ const created = [];
140
+ if (!root) {
141
+ mkdirSync(dirname(target), { recursive: true });
142
+ mkdirSync(target);
143
+ root = lstatSync2(target);
144
+ created.push({ path: target, metadata: root });
145
+ }
146
+ const directories = /* @__PURE__ */ new Map([[target, root]]);
147
+ const directoriesIntact = () => [...directories].every(([path, metadata]) => sameEntry(path, metadata));
148
+ function copy(directory, destination) {
149
+ for (const entry of readdirSync2(directory, { withFileTypes: true })) {
150
+ if (!directoriesIntact())
151
+ throw new Error("The target directory changed during generation.");
152
+ const input = join(directory, entry.name);
153
+ const output = join(destination, entry.name);
154
+ if (entry.isDirectory()) {
155
+ mkdirSync(output);
156
+ const metadata = lstatSync2(output);
157
+ created.push({ path: output, metadata });
158
+ directories.set(output, metadata);
159
+ copy(input, output);
160
+ } else if (entry.isFile()) {
161
+ const contents = readFileSync(input);
162
+ copyFileSync(input, output, constants.COPYFILE_EXCL);
163
+ created.push({ path: output, metadata: lstatSync2(output), contents });
164
+ } else {
165
+ throw new Error(`Unsupported generated entry: ${input}`);
166
+ }
167
+ }
168
+ }
169
+ try {
170
+ copy(source, target);
171
+ } catch (error) {
172
+ for (const entry of created.reverse()) {
173
+ try {
174
+ if (!directoriesIntact() || !sameEntry(entry.path, entry.metadata)) continue;
175
+ if (entry.contents !== void 0) {
176
+ if (readFileSync(entry.path).equals(entry.contents)) unlinkSync(entry.path);
177
+ } else {
178
+ rmdirSync(entry.path);
179
+ directories.delete(entry.path);
180
+ }
181
+ } catch {
182
+ }
183
+ }
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ // src/project.ts
189
+ function projectPackage(options) {
190
+ const typescript = options.language === "ts";
191
+ const dependencies = {};
192
+ const devDependencies = {
193
+ vite: "8.2.2",
194
+ "vite-plugin-shopify-easel": "0.1.0"
195
+ };
196
+ if (typescript) devDependencies.typescript = "5.9.3";
197
+ if (options.framework === "alpine") {
198
+ dependencies.alpinejs = "3.17.1";
199
+ if (typescript) devDependencies["@types/alpinejs"] = "3.13.11";
200
+ }
201
+ if (options.framework === "react") {
202
+ dependencies.react = "19.2.8";
203
+ dependencies["react-dom"] = "19.2.8";
204
+ devDependencies["@vitejs/plugin-react"] = "6.1.1";
205
+ if (typescript) {
206
+ devDependencies["@types/react"] = "19.2.18";
207
+ devDependencies["@types/react-dom"] = "19.2.7";
208
+ }
209
+ }
210
+ if (options.framework === "vue") {
211
+ dependencies.vue = "3.5.42";
212
+ devDependencies["@vitejs/plugin-vue"] = "6.0.8";
213
+ if (typescript) devDependencies["vue-tsc"] = "3.3.11";
214
+ }
215
+ if (options.tailwind) {
216
+ devDependencies.tailwindcss = "4.3.3";
217
+ devDependencies["@tailwindcss/vite"] = "4.3.3";
218
+ }
219
+ const typecheck = options.framework === "vue" ? "vue-tsc" : "tsc";
220
+ return {
221
+ name: packageNameFor(options.target),
222
+ private: true,
223
+ type: "module",
224
+ engines: { node: ">=22.12.0" },
225
+ scripts: {
226
+ dev: "vite",
227
+ build: "vite build",
228
+ check: typescript ? `${typecheck} --noEmit && vite build` : "vite build"
229
+ },
230
+ ...Object.keys(dependencies).length ? { dependencies } : {},
231
+ devDependencies
232
+ };
233
+ }
234
+ function viteConfiguration(options) {
235
+ const imports = [
236
+ "import {defineConfig} from 'vite';",
237
+ "import easel from 'vite-plugin-shopify-easel';"
238
+ ];
239
+ const plugins = [];
240
+ if (options.framework === "react" || options.framework === "vue") {
241
+ imports.push(
242
+ `import ${options.framework} from '@vitejs/plugin-${options.framework}';`
243
+ );
244
+ plugins.push(`${options.framework}()`);
245
+ }
246
+ if (options.tailwind) {
247
+ imports.push("import tailwindcss from '@tailwindcss/vite';");
248
+ plugins.push("tailwindcss()");
249
+ }
250
+ const easel = options.framework === "react" ? `easel({bundles: {theme: {script: 'main.${options.language}x', style: 'style.css'}}})` : "easel()";
251
+ plugins.push(easel);
252
+ return `${imports.join("\n")}
253
+
254
+ export default defineConfig({
255
+ plugins: [${plugins.join(", ")}],
256
+ });
257
+ `;
258
+ }
259
+ function typescriptConfiguration(options) {
260
+ return {
261
+ compilerOptions: {
262
+ target: "ES2022",
263
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
264
+ module: "ESNext",
265
+ moduleResolution: "Bundler",
266
+ moduleDetection: "force",
267
+ strict: true,
268
+ noEmit: true,
269
+ skipLibCheck: true,
270
+ types: ["vite/client"],
271
+ ...options.framework === "react" ? { jsx: "react-jsx" } : {}
272
+ },
273
+ include: ["src", "vite.config.ts"]
274
+ };
275
+ }
276
+ function projectReadme(options) {
277
+ return `# ${options.name}
278
+
279
+ A focused Shopify Liquid starter using Easel, Vite, ${options.language === "ts" ? "TypeScript" : "JavaScript"}, ${options.framework === "none" ? "no UI framework" : options.framework}, and ${options.tailwind ? "Tailwind CSS" : "plain CSS"}.
280
+
281
+ ## Develop
282
+
283
+ Install dependencies with your chosen package manager, then run \`npm run dev\` (or the equivalent \`pnpm\`, \`yarn\`, or \`bun\` script).
284
+
285
+ In a second terminal, run:
286
+
287
+ \`shopify theme dev --store your-store.myshopify.com --notify .easel/shopify-ready\`
288
+
289
+ Shopify CLI is installed and authenticated separately. The notification flag is optional; Vite HMR and Shopify CLI live reload also work independently. Use \`pnpm install --ignore-workspace\` if this theme is nested inside an unrelated pnpm workspace.
290
+
291
+ ## Build and deploy
292
+
293
+ Stop the Vite development server, run \`npm run check\`, then use Shopify CLI to preview or deploy the theme. \`npm run build\` builds without the additional TypeScript check.
294
+
295
+ Deploy \`snippets/easel-assets.liquid\` together with its matching built assets. These generated files are ignored by Git. Never delete unrelated assets or Easel's ownership state to force a build.
296
+
297
+ ## Structure
298
+
299
+ Shopify directories live at the project root. Source lives in \`src/\`; Easel is configured in \`vite.config.${options.language}\`. The homepage contains a small counter to demonstrate the selected tools, with initialization and cleanup for Theme Editor section reloads.
300
+
301
+ This is a minimal starter, not a complete commerce theme: product, collection, cart, and other storefront features are yours to build. Update theme metadata in \`config/settings_schema.json\` for your project.
302
+
303
+ Start with one shared entry. Add named page bundles only when selective loading is useful; see [the named bundles example](https://github.com/BillyNoyes/Easel/tree/main/examples/named-bundles).
304
+
305
+ [Easel documentation](https://easel.billynoyes.co.uk/docs/)
306
+ `;
307
+ }
308
+
309
+ // src/generate.ts
310
+ var templates = fileURLToPath(new URL("../dist/templates/", import.meta.url));
311
+ function generateTheme(options, templateRoot = templates) {
312
+ validateOptions(options);
313
+ const target = resolve2(options.target);
314
+ assertTargetDirectory(target);
315
+ const staging = mkdtempSync(join2(tmpdir(), "easel-create-"));
316
+ try {
317
+ renderTheme(options, staging, templateRoot);
318
+ writeProject(staging, target);
319
+ return target;
320
+ } finally {
321
+ rmSync(staging, { recursive: true, force: true });
322
+ }
323
+ }
324
+ function renderTheme(options, target, templateRoot) {
325
+ const source = join2(templateRoot, options.language);
326
+ cpSync(join2(source, "base"), target, {
327
+ recursive: true,
328
+ force: false,
329
+ errorOnExist: true
330
+ });
331
+ if (options.framework !== "none") {
332
+ rmSync(join2(target, "src", `main.${options.language}`));
333
+ cpSync(join2(source, options.framework), target, { recursive: true });
334
+ }
335
+ for (const directory of [
336
+ "assets",
337
+ "blocks",
338
+ "config",
339
+ "layout",
340
+ "locales",
341
+ "sections",
342
+ "snippets",
343
+ "src",
344
+ "templates"
345
+ ]) {
346
+ mkdirSync2(join2(target, directory), { recursive: true });
347
+ }
348
+ renameSync(join2(target, "_gitignore"), join2(target, ".gitignore"));
349
+ const writeJson = (path, data) => writeFileSync(join2(target, path), `${JSON.stringify(data, null, 2)}
350
+ `);
351
+ writeJson("package.json", projectPackage(options));
352
+ writeFileSync(
353
+ join2(target, `vite.config.${options.language}`),
354
+ viteConfiguration(options)
355
+ );
356
+ if (options.language === "ts")
357
+ writeJson("tsconfig.json", typescriptConfiguration(options));
358
+ writeFileSync(join2(target, "README.md"), projectReadme(options));
359
+ for (const file of ["config/settings_schema.json", "locales/en.default.schema.json"]) {
360
+ const path = join2(target, file);
361
+ writeFileSync(
362
+ path,
363
+ readFileSync2(path, "utf8").replaceAll("__THEME_NAME__", options.name)
364
+ );
365
+ }
366
+ if (options.framework === "alpine")
367
+ rmSync(join2(target, "src", `sections.${options.language}`));
368
+ if (options.tailwind) {
369
+ const path = join2(target, "src/style.css");
370
+ writeFileSync(
371
+ path,
372
+ `@import 'tailwindcss';
373
+ @source '../layout';
374
+ @source '../sections';
375
+ @source '../snippets';
376
+ @source '../blocks';
377
+ @source '../templates';
378
+
379
+ ${readFileSync2(path, "utf8")}`
380
+ );
381
+ const hero = join2(target, "sections/hero.liquid");
382
+ writeFileSync(
383
+ hero,
384
+ readFileSync2(hero, "utf8").replace(
385
+ 'class="hero"',
386
+ 'class="hero rounded-xl border"'
387
+ )
388
+ );
389
+ }
390
+ }
391
+
392
+ // src/install.ts
393
+ import spawn from "cross-spawn";
394
+ function installArguments(manager) {
395
+ return manager === "pnpm" ? ["install", "--ignore-workspace"] : ["install"];
396
+ }
397
+ var InstallError = class extends Error {
398
+ constructor(message, cancelled) {
399
+ super(message);
400
+ this.cancelled = cancelled;
401
+ }
402
+ cancelled;
403
+ };
404
+ function installDependencies(target, manager) {
405
+ return new Promise((resolve4, reject) => {
406
+ let child;
407
+ let interrupted = false;
408
+ const interrupt = () => {
409
+ interrupted = true;
410
+ child?.kill("SIGINT");
411
+ };
412
+ process.on("SIGINT", interrupt);
413
+ const cleanup = () => process.removeListener("SIGINT", interrupt);
414
+ try {
415
+ child = spawn(manager, installArguments(manager), { cwd: target, stdio: "inherit" });
416
+ } catch (error) {
417
+ cleanup();
418
+ reject(
419
+ new InstallError(
420
+ `Could not run ${manager}: ${error instanceof Error ? error.message : String(error)}`,
421
+ interrupted
422
+ )
423
+ );
424
+ return;
425
+ }
426
+ if (interrupted) child.kill("SIGINT");
427
+ child.on("error", (error) => {
428
+ cleanup();
429
+ reject(new InstallError(`Could not run ${manager}: ${error.message}`, interrupted));
430
+ });
431
+ child.on("close", (code, signal) => {
432
+ cleanup();
433
+ if (code === 0 && !interrupted) resolve4();
434
+ else
435
+ reject(
436
+ new InstallError(
437
+ `${manager} install ${interrupted || signal ? "was interrupted" : `exited with code ${String(code)}`}.`,
438
+ interrupted || signal === "SIGINT"
439
+ )
440
+ );
441
+ });
442
+ });
443
+ }
444
+ function changeDirectoryCommand(path, platform = process.platform) {
445
+ if (platform === "win32")
446
+ return `Set-Location -LiteralPath '${path.replaceAll("'", "''")}'`;
447
+ return `cd '${path.replaceAll("'", "'\\''")}'`;
448
+ }
449
+
450
+ // src/cli.ts
451
+ var help = `Usage: create-easel-theme <directory> [options]
452
+
453
+ Create a Shopify Liquid theme with Easel and Vite.
454
+
455
+ --language ts|js Language (default: ts)
456
+ --framework none|alpine|react|vue UI tools (default: none)
457
+ --tailwind / --no-tailwind Tailwind CSS (default: plain CSS)
458
+ --name <name> Theme display name
459
+ --package-manager npm|pnpm|yarn|bun
460
+ --install / --no-install Install dependencies
461
+ --yes, -y Use defaults without prompts
462
+ --no-interactive Disable prompts
463
+ --help, -h Show help
464
+ --version, -v Show version
465
+
466
+ Non-interactive runs require a directory and only install with --install.
467
+ Use . for the current directory, empty apart from .git or .DS_Store.
468
+ Existing project files are never overwritten. Shopify CLI remains separate.
469
+ `;
470
+ var Cancelled = class extends Error {
471
+ };
472
+ async function ask(answer) {
473
+ const value = await answer;
474
+ if (p.isCancel(value)) throw new Cancelled();
475
+ return value;
476
+ }
477
+ function targetError(target) {
478
+ try {
479
+ if (!target) throw new Error("Provide a directory for your theme.");
480
+ packageNameFor(target);
481
+ assertTargetDirectory(resolve3(target));
482
+ } catch (error) {
483
+ return error instanceof Error ? error.message : String(error);
484
+ }
485
+ }
486
+ async function main() {
487
+ let interactive = false;
488
+ try {
489
+ const args = parseArguments(process.argv.slice(2));
490
+ if (args.help) {
491
+ console.log(help);
492
+ return;
493
+ }
494
+ if (args.version) {
495
+ console.log(version);
496
+ return;
497
+ }
498
+ const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
499
+ if (args.interactive && !tty)
500
+ throw new Error(
501
+ "Interactive mode requires a terminal. Supply a directory and flags instead."
502
+ );
503
+ const ci = Boolean(process.env.CI && !["false", "0"].includes(process.env.CI));
504
+ interactive = tty && !args.yes && (args.interactive ?? !ci);
505
+ if (interactive) p.intro(`Create an Easel theme \xB7 ${version}`);
506
+ const target = args.target ?? (interactive ? await ask(
507
+ p.text({
508
+ message: "Where should we create your theme? (. for current directory)",
509
+ placeholder: "my-theme",
510
+ defaultValue: "my-theme",
511
+ validate: (value) => targetError(value || "my-theme")
512
+ })
513
+ ) : void 0);
514
+ const problem = targetError(target);
515
+ if (problem || !target) throw new Error(problem ?? "Provide a target directory.");
516
+ const language = choice(
517
+ args.language ?? (interactive ? await ask(
518
+ p.select({
519
+ message: "Which language?",
520
+ initialValue: "ts",
521
+ options: [
522
+ { value: "ts", label: "TypeScript", hint: "recommended" },
523
+ { value: "js", label: "JavaScript" }
524
+ ]
525
+ })
526
+ ) : "ts"),
527
+ languages,
528
+ "--language"
529
+ );
530
+ const framework = choice(
531
+ args.framework ?? (interactive ? await ask(
532
+ p.select({
533
+ message: "Which UI tools?",
534
+ initialValue: "none",
535
+ options: [
536
+ { value: "none", label: "None", hint: "plain browser APIs" },
537
+ { value: "alpine", label: "Alpine" },
538
+ { value: "react", label: "React" },
539
+ { value: "vue", label: "Vue" }
540
+ ]
541
+ })
542
+ ) : "none"),
543
+ frameworks,
544
+ "--framework"
545
+ );
546
+ const tailwind = args.tailwind ?? (interactive ? await ask(p.confirm({ message: "Add Tailwind CSS?", initialValue: false })) : false);
547
+ const manager = choice(
548
+ args["package-manager"] ?? detectPackageManager(),
549
+ packageManagers,
550
+ "--package-manager"
551
+ );
552
+ const install = args.install ?? (interactive ? await ask(
553
+ p.confirm({
554
+ message: `Install dependencies with ${manager}? This runs package install scripts.`,
555
+ initialValue: true
556
+ })
557
+ ) : false);
558
+ const options = {
559
+ target,
560
+ name: args.name ?? defaultName(target),
561
+ language,
562
+ framework,
563
+ tailwind
564
+ };
565
+ if (interactive) p.log.step("Creating your theme\u2026");
566
+ const created = generateTheme(options);
567
+ if (interactive) p.log.success(`Created ${created}`);
568
+ else console.log(`Created ${created}`);
569
+ let installed = false;
570
+ if (install) {
571
+ if (interactive) p.log.step(`Installing with ${manager}\u2026`);
572
+ try {
573
+ await installDependencies(created, manager);
574
+ installed = true;
575
+ } catch (error) {
576
+ console.error(error instanceof Error ? error.message : String(error));
577
+ console.error(
578
+ "Your theme files have been kept. You can retry installation below."
579
+ );
580
+ process.exitCode = error instanceof InstallError && error.cancelled ? 130 : 1;
581
+ }
582
+ }
583
+ const steps = created === process.cwd() ? [] : [changeDirectoryCommand(created)];
584
+ if (!installed) steps.push(`${manager} ${installArguments(manager).join(" ")}`);
585
+ steps.push(`${manager} run dev`);
586
+ const message = `${steps.join("\n")}
587
+
588
+ In another terminal:
589
+ shopify theme dev --store your-store.myshopify.com --notify .easel/shopify-ready
590
+
591
+ Shopify CLI must be installed and authenticated separately.`;
592
+ if (interactive) {
593
+ p.note(message, "Next steps");
594
+ p.outro("Your theme is yours to build.");
595
+ } else console.log(`
596
+ ${message}`);
597
+ } catch (error) {
598
+ const message = error instanceof Cancelled ? "Operation cancelled. No theme created." : error instanceof Error ? error.message : String(error);
599
+ if (interactive) p.cancel(message);
600
+ else console.error(message);
601
+ process.exitCode = error instanceof Cancelled ? 130 : 1;
602
+ }
603
+ }
604
+ await main();
@@ -0,0 +1,9 @@
1
+ {% doc %}
2
+ Disabled fallback buttons become interactive when Alpine initializes this section.
3
+ {% enddoc %}
4
+
5
+ <div class="counter" x-data="easelCounter">
6
+ <button type="button" aria-label="{{ 'general.counter.decrease' | t | escape }}" disabled :disabled="false" @click="count -= 1">−</button>
7
+ <output x-text="count">0</output>
8
+ <button type="button" aria-label="{{ 'general.counter.increase' | t | escape }}" disabled :disabled="false" @click="count += 1">+</button>
9
+ </div>
@@ -0,0 +1,19 @@
1
+ import Alpine from 'alpinejs';
2
+ Alpine.data('easelCounter', () => ({ count: 0 }));
3
+ Alpine.start();
4
+ function handleSectionLoad(event) {
5
+ if (event.target instanceof HTMLElement)
6
+ Alpine.initTree(event.target);
7
+ }
8
+ function handleSectionUnload(event) {
9
+ if (event.target instanceof HTMLElement)
10
+ Alpine.destroyTree(event.target);
11
+ }
12
+ document.addEventListener('shopify:section:load', handleSectionLoad);
13
+ document.addEventListener('shopify:section:unload', handleSectionUnload);
14
+ if (import.meta.hot) {
15
+ import.meta.hot.dispose(() => {
16
+ document.removeEventListener('shopify:section:load', handleSectionLoad);
17
+ document.removeEventListener('shopify:section:unload', handleSectionUnload);
18
+ });
19
+ }
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ .easel/
3
+ *.zip
4
+ assets/easel-*
5
+ snippets/easel-assets.liquid
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="16" fill="#171717"/>
3
+ <path d="M32 9v8M23 41l-6 14m24-14 6 14M14 41h36M20 17h24v18H20z" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
@@ -0,0 +1,12 @@
1
+ {
2
+ "current": {
3
+ "background": "#f5f2eb",
4
+ "foreground": "#171714"
5
+ },
6
+ "presets": {
7
+ "Default": {
8
+ "background": "#f5f2eb",
9
+ "foreground": "#171714"
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,27 @@
1
+ [
2
+ {
3
+ "name": "theme_info",
4
+ "theme_name": "__THEME_NAME__",
5
+ "theme_version": "0.1.0",
6
+ "theme_author": "Billy Noyes",
7
+ "theme_documentation_url": "https://github.com/BillyNoyes/Easel",
8
+ "theme_support_url": "https://github.com/BillyNoyes/Easel/issues"
9
+ },
10
+ {
11
+ "name": "t:settings.colors.name",
12
+ "settings": [
13
+ {
14
+ "type": "color",
15
+ "id": "background",
16
+ "label": "t:settings.colors.background",
17
+ "default": "#f5f2eb"
18
+ },
19
+ {
20
+ "type": "color",
21
+ "id": "foreground",
22
+ "label": "t:settings.colors.foreground",
23
+ "default": "#171714"
24
+ }
25
+ ]
26
+ }
27
+ ]