climaybe 2.4.2 → 3.0.1
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 +25 -26
- package/bin/version.txt +1 -1
- package/package.json +9 -1
- package/src/commands/add-dev-kit.js +5 -3
- package/src/commands/add-store.js +22 -7
- package/src/commands/app-init.js +1 -1
- package/src/commands/build-scripts.js +27 -0
- package/src/commands/create-entrypoints.js +45 -0
- package/src/commands/ensure-branches.js +4 -9
- package/src/commands/init.js +69 -18
- package/src/commands/migrate-legacy-config.js +217 -0
- package/src/index.js +37 -2
- package/src/lib/build-scripts.js +153 -0
- package/src/lib/build-workflows.js +3 -43
- package/src/lib/config.js +78 -8
- package/src/lib/dev-runtime.js +210 -0
- package/src/lib/prompts.js +29 -6
- package/src/lib/shopify-cli.js +30 -0
- package/src/lib/theme-dev-kit.js +52 -52
- package/src/lib/watch.js +80 -0
- package/src/lib/workflows.js +2 -4
- package/src/workflows/build/create-release.yml +23 -6
- package/src/workflows/build/reusable-build.yml +26 -6
- package/src/workflows/multi/main-to-staging-stores.yml +4 -3
- package/src/workflows/multi/multistore-hotfix-to-main.yml +3 -3
- package/src/workflows/multi/pr-to-live.yml +3 -3
- package/src/workflows/preview/pr-close.yml +4 -4
- package/src/workflows/preview/pr-update.yml +4 -4
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { migrateLegacyPackageConfigToClimaybe, readClimaybeConfig, readPkg, writePkg } from '../lib/config.js';
|
|
6
|
+
import { updateWorkflowsCommand } from './update-workflows.js';
|
|
7
|
+
import { requireThemeProject } from '../lib/theme-guard.js';
|
|
8
|
+
import { getDevKitExistingFiles, scaffoldThemeDevKit } from '../lib/theme-dev-kit.js';
|
|
9
|
+
|
|
10
|
+
const LEGACY_SCRIPT_KEYS = [
|
|
11
|
+
'shopify:serve',
|
|
12
|
+
'shopify:serve:sync',
|
|
13
|
+
'shopify:populate',
|
|
14
|
+
'scripts:build',
|
|
15
|
+
'scripts:watch',
|
|
16
|
+
'tailwind:watch',
|
|
17
|
+
'tailwind:build',
|
|
18
|
+
'dev',
|
|
19
|
+
'dev:theme',
|
|
20
|
+
'dev:assets',
|
|
21
|
+
'dev:no-sync',
|
|
22
|
+
'lint:liquid',
|
|
23
|
+
'lint:js',
|
|
24
|
+
'lint:css',
|
|
25
|
+
'release',
|
|
26
|
+
'prepare',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const LEGACY_DEP_NAMES = [
|
|
30
|
+
'concurrently',
|
|
31
|
+
'nodemon',
|
|
32
|
+
'tailwindcss',
|
|
33
|
+
'@tailwindcss/cli',
|
|
34
|
+
'@tailwindcss/typography',
|
|
35
|
+
'eslint',
|
|
36
|
+
'@eslint/js',
|
|
37
|
+
'@eslint/create-config',
|
|
38
|
+
'stylelint',
|
|
39
|
+
'stylelint-config-standard',
|
|
40
|
+
'prettier',
|
|
41
|
+
'@shopify/prettier-plugin-liquid',
|
|
42
|
+
'@commitlint/cli',
|
|
43
|
+
'@commitlint/config-conventional',
|
|
44
|
+
'husky',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function cleanupLegacyFiles(cwd) {
|
|
48
|
+
const candidates = [
|
|
49
|
+
'.climaybe/build-scripts.js',
|
|
50
|
+
'build-scripts.js',
|
|
51
|
+
'.climaybe/dev-with-sync.sh',
|
|
52
|
+
'.climaybe/dev.sh',
|
|
53
|
+
];
|
|
54
|
+
const removed = [];
|
|
55
|
+
for (const rel of candidates) {
|
|
56
|
+
const abs = join(cwd, rel);
|
|
57
|
+
if (!existsSync(abs)) continue;
|
|
58
|
+
rmSync(abs, { force: true });
|
|
59
|
+
removed.push(rel);
|
|
60
|
+
}
|
|
61
|
+
return removed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cleanupLegacyPackageJson(cwd) {
|
|
65
|
+
const pkg = readPkg(cwd);
|
|
66
|
+
if (!pkg || typeof pkg !== 'object') return { removedScripts: [], removedDeps: [] };
|
|
67
|
+
|
|
68
|
+
const removedScripts = [];
|
|
69
|
+
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
70
|
+
for (const key of LEGACY_SCRIPT_KEYS) {
|
|
71
|
+
if (key in pkg.scripts) {
|
|
72
|
+
delete pkg.scripts[key];
|
|
73
|
+
removedScripts.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (Object.keys(pkg.scripts).length === 0) delete pkg.scripts;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const removedDeps = [];
|
|
80
|
+
for (const depField of ['dependencies', 'devDependencies']) {
|
|
81
|
+
const deps = pkg[depField];
|
|
82
|
+
if (!deps || typeof deps !== 'object') continue;
|
|
83
|
+
for (const name of LEGACY_DEP_NAMES) {
|
|
84
|
+
if (name in deps) {
|
|
85
|
+
delete deps[name];
|
|
86
|
+
removedDeps.push(`${depField}:${name}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(deps).length === 0) delete pkg[depField];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
writePkg(pkg, cwd);
|
|
93
|
+
return { removedScripts, removedDeps };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function migrateLegacyConfigCommand(options = {}) {
|
|
97
|
+
const overwrite = options.overwrite === true;
|
|
98
|
+
const updateWorkflows = options.updateWorkflows !== false;
|
|
99
|
+
const yes = options.yes === true;
|
|
100
|
+
|
|
101
|
+
console.log(pc.bold('\n climaybe — Migrate legacy config\n'));
|
|
102
|
+
|
|
103
|
+
if (!requireThemeProject()) return;
|
|
104
|
+
|
|
105
|
+
const legacy = readPkg()?.config ?? null;
|
|
106
|
+
if (!legacy || typeof legacy !== 'object') {
|
|
107
|
+
console.log(pc.yellow(' No legacy package.json config found (package.json → config).'));
|
|
108
|
+
console.log(pc.dim(' Nothing to migrate.\n'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const existing = readClimaybeConfig();
|
|
113
|
+
if (existing && !overwrite) {
|
|
114
|
+
const ok = yes
|
|
115
|
+
? true
|
|
116
|
+
: (
|
|
117
|
+
await prompts({
|
|
118
|
+
type: 'confirm',
|
|
119
|
+
name: 'ok',
|
|
120
|
+
message: 'A climaybe.config.json already exists. Overwrite it from package.json config?',
|
|
121
|
+
initial: false,
|
|
122
|
+
})
|
|
123
|
+
).ok;
|
|
124
|
+
if (!ok) {
|
|
125
|
+
console.log(pc.dim(' Cancelled.\n'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const did = migrateLegacyPackageConfigToClimaybe({ overwrite: true });
|
|
131
|
+
if (!did) {
|
|
132
|
+
console.log(pc.yellow(' Migration skipped (no legacy config, or config already present).'));
|
|
133
|
+
console.log(pc.dim(' Tip: run with --overwrite to force rewrite.\n'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(pc.green(' Migrated package.json config → climaybe.config.json'));
|
|
138
|
+
|
|
139
|
+
// Clean legacy dev system artifacts (sample-style package scripts, shims, old .climaybe scripts).
|
|
140
|
+
const cleanup = yes
|
|
141
|
+
? true
|
|
142
|
+
: (
|
|
143
|
+
await prompts({
|
|
144
|
+
type: 'confirm',
|
|
145
|
+
name: 'cleanup',
|
|
146
|
+
message: 'Remove legacy dev scripts/deps from package.json and delete old local shims?',
|
|
147
|
+
initial: true,
|
|
148
|
+
})
|
|
149
|
+
).cleanup;
|
|
150
|
+
if (cleanup) {
|
|
151
|
+
const removedFiles = cleanupLegacyFiles(process.cwd());
|
|
152
|
+
const { removedScripts, removedDeps } = cleanupLegacyPackageJson(process.cwd());
|
|
153
|
+
if (removedFiles.length > 0) {
|
|
154
|
+
console.log(pc.green(` Removed legacy files: ${removedFiles.join(', ')}`));
|
|
155
|
+
}
|
|
156
|
+
if (removedScripts.length > 0) {
|
|
157
|
+
console.log(pc.green(` Removed legacy package.json scripts: ${removedScripts.join(', ')}`));
|
|
158
|
+
}
|
|
159
|
+
if (removedDeps.length > 0) {
|
|
160
|
+
console.log(pc.green(` Removed legacy package.json deps: ${removedDeps.join(', ')}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Reinstall dev kit so tasks/theme-check config match the new climaybe runtime.
|
|
165
|
+
const reinstallDevKit = yes
|
|
166
|
+
? true
|
|
167
|
+
: (
|
|
168
|
+
await prompts({
|
|
169
|
+
type: 'confirm',
|
|
170
|
+
name: 'reinstallDevKit',
|
|
171
|
+
message: 'Install/refresh climaybe dev kit files (.vscode/tasks.json, etc.)?',
|
|
172
|
+
initial: true,
|
|
173
|
+
})
|
|
174
|
+
).reinstallDevKit;
|
|
175
|
+
if (reinstallDevKit) {
|
|
176
|
+
const existing = getDevKitExistingFiles({ includeVSCodeTasks: true });
|
|
177
|
+
if (existing.length > 0) {
|
|
178
|
+
console.log(pc.yellow(' Some dev kit files already exist and will be replaced:'));
|
|
179
|
+
for (const path of existing) console.log(pc.yellow(` - ${path}`));
|
|
180
|
+
const ok = yes
|
|
181
|
+
? true
|
|
182
|
+
: (
|
|
183
|
+
await prompts({
|
|
184
|
+
type: 'confirm',
|
|
185
|
+
name: 'ok',
|
|
186
|
+
message: 'Replace these files?',
|
|
187
|
+
initial: true,
|
|
188
|
+
})
|
|
189
|
+
).ok;
|
|
190
|
+
if (!ok) {
|
|
191
|
+
console.log(pc.dim(' Skipped dev kit refresh.\n'));
|
|
192
|
+
} else {
|
|
193
|
+
const legacy = readPkg()?.config ?? {};
|
|
194
|
+
scaffoldThemeDevKit({
|
|
195
|
+
includeVSCodeTasks: true,
|
|
196
|
+
defaultStoreDomain: legacy.default_store || legacy.store || '',
|
|
197
|
+
});
|
|
198
|
+
console.log(pc.green(' Dev kit refreshed.'));
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const legacy = readPkg()?.config ?? {};
|
|
202
|
+
scaffoldThemeDevKit({
|
|
203
|
+
includeVSCodeTasks: true,
|
|
204
|
+
defaultStoreDomain: legacy.default_store || legacy.store || '',
|
|
205
|
+
});
|
|
206
|
+
console.log(pc.green(' Dev kit installed.'));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (updateWorkflows) {
|
|
211
|
+
console.log(pc.dim('\n Updating workflows to use climaybe.config.json...\n'));
|
|
212
|
+
await updateWorkflowsCommand();
|
|
213
|
+
} else {
|
|
214
|
+
console.log(pc.dim(' Skipped workflow update (pass --update-workflows to refresh).\n'));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,10 @@ import { setupCommitlintCommand } from './commands/setup-commitlint.js';
|
|
|
9
9
|
import { addCursorSkillCommand } from './commands/add-cursor-skill.js';
|
|
10
10
|
import { addDevKitCommand } from './commands/add-dev-kit.js';
|
|
11
11
|
import { appInitCommand } from './commands/app-init.js';
|
|
12
|
+
import { migrateLegacyConfigCommand } from './commands/migrate-legacy-config.js';
|
|
13
|
+
import { buildScriptsCommand } from './commands/build-scripts.js';
|
|
14
|
+
import { createEntrypointsCommand } from './commands/create-entrypoints.js';
|
|
15
|
+
import { serveAll, serveAssets, serveShopify, lintAll, buildAll } from './lib/dev-runtime.js';
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Register theme CI/CD commands on a Commander instance (root or `theme` subgroup).
|
|
@@ -44,9 +48,38 @@ function registerThemeCommands(cmd) {
|
|
|
44
48
|
|
|
45
49
|
cmd
|
|
46
50
|
.command('add-dev-kit')
|
|
47
|
-
.description('Install/update local theme dev kit files (
|
|
51
|
+
.description('Install/update local theme dev kit files (configs, ignores, optional VS Code tasks)')
|
|
48
52
|
.action(addDevKitCommand);
|
|
49
53
|
|
|
54
|
+
cmd
|
|
55
|
+
.command('migrate-legacy-config')
|
|
56
|
+
.description('Migrate legacy package.json config to climaybe.config.json (optional workflow refresh)')
|
|
57
|
+
.option('--overwrite', 'Overwrite existing climaybe.config.json')
|
|
58
|
+
.option('-y, --yes', 'Non-interactive; assume "yes" for prompts')
|
|
59
|
+
.option('--no-update-workflows', 'Do not refresh workflows after migrating')
|
|
60
|
+
.action(migrateLegacyConfigCommand);
|
|
61
|
+
|
|
62
|
+
cmd
|
|
63
|
+
.command('serve')
|
|
64
|
+
.description('Run local theme dev (Shopify + assets + Theme Check)')
|
|
65
|
+
.option('--no-theme-check', 'Disable Theme Check watcher')
|
|
66
|
+
.action((opts) => serveAll({ includeThemeCheck: opts.themeCheck !== false }));
|
|
67
|
+
cmd.command('serve:shopify').description('Run Shopify theme dev server').action(() => serveShopify());
|
|
68
|
+
cmd
|
|
69
|
+
.command('serve:assets')
|
|
70
|
+
.description('Run assets watch (Tailwind + scripts + Theme Check)')
|
|
71
|
+
.option('--no-theme-check', 'Disable Theme Check watcher')
|
|
72
|
+
.action((opts) => serveAssets({ includeThemeCheck: opts.themeCheck !== false }));
|
|
73
|
+
|
|
74
|
+
cmd.command('lint').description('Run theme linting (liquid, js, css)').action(() => lintAll());
|
|
75
|
+
|
|
76
|
+
cmd.command('build').description('Build assets (Tailwind + scripts build)').action(() => buildAll());
|
|
77
|
+
cmd.command('build-scripts').description('Build _scripts → assets/index.js').action(buildScriptsCommand);
|
|
78
|
+
cmd
|
|
79
|
+
.command('create-entrypoints')
|
|
80
|
+
.description('Create _scripts/main.js and _styles/main.css (optional)')
|
|
81
|
+
.action(createEntrypointsCommand);
|
|
82
|
+
|
|
50
83
|
cmd
|
|
51
84
|
.command('update-workflows')
|
|
52
85
|
.description('Refresh GitHub Actions workflows from latest bundled templates')
|
|
@@ -84,7 +117,7 @@ export function createProgram(version = '0.0.0', packageDir = '') {
|
|
|
84
117
|
const app = program.command('app').description('Shopify app repo helpers (no theme workflows)');
|
|
85
118
|
app
|
|
86
119
|
.command('init')
|
|
87
|
-
.description('Set up commitlint, Cursor bundle (rules/skills/agents), and project_type: app in
|
|
120
|
+
.description('Set up commitlint, Cursor bundle (rules/skills/agents), and project_type: app in climaybe.config.json')
|
|
88
121
|
.action(appInitCommand);
|
|
89
122
|
|
|
90
123
|
program
|
|
@@ -104,5 +137,7 @@ export function createProgram(version = '0.0.0', packageDir = '') {
|
|
|
104
137
|
}
|
|
105
138
|
|
|
106
139
|
export function run(argv, version, packageDir = '') {
|
|
140
|
+
if (packageDir) process.env.CLIMAYBE_PACKAGE_DIR = packageDir;
|
|
141
|
+
if (version) process.env.CLIMAYBE_PACKAGE_VERSION = version;
|
|
107
142
|
createProgram(version, packageDir).parse(argv);
|
|
108
143
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, basename, dirname, normalize } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function extractImports(content) {
|
|
5
|
+
const imports = [];
|
|
6
|
+
// Supports compact imports (import{a}from"./x"), multiline forms,
|
|
7
|
+
// and import attributes (with { type: "json" }).
|
|
8
|
+
const fromImportRegex =
|
|
9
|
+
/(^|\n)\s*import(?:\s+type)?\s*[\s\S]*?\s*\bfrom\b\s*['"]([^'"]+)['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g;
|
|
10
|
+
const sideEffectImportRegex = /(^|\n)\s*import\s*['"]([^'"]+)['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g;
|
|
11
|
+
let match;
|
|
12
|
+
|
|
13
|
+
while ((match = fromImportRegex.exec(content)) !== null) {
|
|
14
|
+
imports.push(match[2]);
|
|
15
|
+
}
|
|
16
|
+
while ((match = sideEffectImportRegex.exec(content)) !== null) {
|
|
17
|
+
imports.push(match[2]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return imports;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stripModuleSyntax(content) {
|
|
24
|
+
// Remove import statements (including multiline/compact forms and import attributes).
|
|
25
|
+
let cleaned = content.replace(
|
|
26
|
+
/(^|\n)\s*import(?:\s+type)?\s*[\s\S]*?\s*\bfrom\b\s*['"][^'"]+['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g,
|
|
27
|
+
'$1'
|
|
28
|
+
);
|
|
29
|
+
cleaned = cleaned.replace(/(^|\n)\s*import\s*['"][^'"]+['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g, '$1');
|
|
30
|
+
|
|
31
|
+
// Fallback: ensure no standalone import declarations leak into bundle output.
|
|
32
|
+
cleaned = cleaned.replace(/^[ \t]*import\s*['"][^'"]+['"][ \t]*;?[ \t]*$/gm, '');
|
|
33
|
+
cleaned = cleaned.replace(
|
|
34
|
+
/^[ \t]*import(?:\s+type)?[ \t]*[^;\n\r]*\bfrom\b[ \t]*['"][^'"]+['"][ \t]*(?:with[ \t]*\{[^}\n\r]*\})?[ \t]*;?[ \t]*$/gm,
|
|
35
|
+
''
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
cleaned = cleaned.replace(/^\s*export\s+default\s+/gm, '');
|
|
39
|
+
cleaned = cleaned.replace(/^\s*export\s+\{[^}]*\}\s*;?\s*$/gm, '');
|
|
40
|
+
cleaned = cleaned.replace(/^\s*export\s+(?=(const|let|var|function|class)\b)/gm, '');
|
|
41
|
+
return cleaned;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function processScriptFile({ scriptsDir, filePath, processedFiles }) {
|
|
45
|
+
if (processedFiles.has(filePath)) return '';
|
|
46
|
+
processedFiles.add(filePath);
|
|
47
|
+
|
|
48
|
+
const fullPath = join(scriptsDir, filePath);
|
|
49
|
+
if (!existsSync(fullPath)) {
|
|
50
|
+
// Keep going; missing optional imports shouldn't hard fail local dev
|
|
51
|
+
// (CI will still fail later if output is invalid).
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.warn(`Warning: File ${filePath} not found`);
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let content = readFileSync(fullPath, 'utf8');
|
|
58
|
+
const imports = extractImports(content);
|
|
59
|
+
|
|
60
|
+
let importedContent = '';
|
|
61
|
+
for (const importPath of imports) {
|
|
62
|
+
const resolvedImport = resolveImportPath(filePath, importPath);
|
|
63
|
+
if (!resolvedImport) continue;
|
|
64
|
+
importedContent += processScriptFile({ scriptsDir, filePath: resolvedImport, processedFiles });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
content = stripModuleSyntax(content);
|
|
68
|
+
|
|
69
|
+
if (process.env.NODE_ENV === 'production') {
|
|
70
|
+
content = content.replace(/\/\*\*[\s\S]*?\*\//g, '');
|
|
71
|
+
content = content.replace(/^\s*\*.*$/gm, '');
|
|
72
|
+
content = content.replace(/console\.(log|warn|error)\([^)]*\);?\s*/g, '');
|
|
73
|
+
content = content.replace(/^\s*\n/gm, '');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return importedContent + '\n' + content;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveImportPath(fromFilePath, importPath) {
|
|
80
|
+
// Only bundle local relative imports from _scripts.
|
|
81
|
+
if (!importPath || (!importPath.startsWith('./') && !importPath.startsWith('../'))) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const fromDir = dirname(fromFilePath);
|
|
85
|
+
const resolved = normalize(join(fromDir, importPath)).replace(/\\/g, '/');
|
|
86
|
+
return resolved.startsWith('./') ? resolved.slice(2) : resolved;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function collectImportedFiles({ scriptsDir, entryFile, seen = new Set() }) {
|
|
90
|
+
if (seen.has(entryFile)) return seen;
|
|
91
|
+
seen.add(entryFile);
|
|
92
|
+
|
|
93
|
+
const fullPath = join(scriptsDir, entryFile);
|
|
94
|
+
if (!existsSync(fullPath)) return seen;
|
|
95
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
96
|
+
const imports = extractImports(content);
|
|
97
|
+
for (const importPath of imports) {
|
|
98
|
+
const resolved = resolveImportPath(entryFile, importPath);
|
|
99
|
+
if (!resolved) continue;
|
|
100
|
+
collectImportedFiles({ scriptsDir, entryFile: resolved, seen });
|
|
101
|
+
}
|
|
102
|
+
return seen;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function listTopLevelEntrypoints(scriptsDir) {
|
|
106
|
+
if (!existsSync(scriptsDir)) return [];
|
|
107
|
+
return readdirSync(scriptsDir, { withFileTypes: true })
|
|
108
|
+
.filter((d) => d.isFile() && d.name.endsWith('.js'))
|
|
109
|
+
.map((d) => d.name)
|
|
110
|
+
.sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function outputNameForEntrypoint(entryFile) {
|
|
114
|
+
if (entryFile === 'main.js') return 'index.js';
|
|
115
|
+
return basename(entryFile);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildSingleEntrypoint({ cwd, entryFile }) {
|
|
119
|
+
const scriptsDir = join(cwd, '_scripts');
|
|
120
|
+
const entryPath = join(scriptsDir, entryFile);
|
|
121
|
+
if (!existsSync(entryPath)) {
|
|
122
|
+
throw new Error(`Missing required file: _scripts/${entryFile}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const processedFiles = new Set();
|
|
126
|
+
let finalContent = processScriptFile({ scriptsDir, filePath: entryFile, processedFiles });
|
|
127
|
+
finalContent = stripModuleSyntax(finalContent);
|
|
128
|
+
|
|
129
|
+
const assetsDir = join(cwd, 'assets');
|
|
130
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
131
|
+
|
|
132
|
+
const outFile = outputNameForEntrypoint(entryFile);
|
|
133
|
+
const outputPath = join(assetsDir, outFile);
|
|
134
|
+
writeFileSync(outputPath, finalContent.trim() + '\n', 'utf-8');
|
|
135
|
+
|
|
136
|
+
return { entryFile, fileCount: processedFiles.size, outputPath };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildScripts({ cwd = process.cwd(), entry = null } = {}) {
|
|
140
|
+
const scriptsDir = join(cwd, '_scripts');
|
|
141
|
+
let entrypoints = entry ? [entry.endsWith('.js') ? entry : `${entry}.js`] : listTopLevelEntrypoints(scriptsDir);
|
|
142
|
+
if (!entry && entrypoints.includes('main.js')) {
|
|
143
|
+
const importedByMain = collectImportedFiles({ scriptsDir, entryFile: 'main.js' });
|
|
144
|
+
importedByMain.delete('main.js');
|
|
145
|
+
entrypoints = entrypoints.filter((ep) => ep === 'main.js' || !importedByMain.has(ep));
|
|
146
|
+
}
|
|
147
|
+
if (entrypoints.length === 0) {
|
|
148
|
+
return { bundles: [] };
|
|
149
|
+
}
|
|
150
|
+
const bundles = entrypoints.map((entryFile) => buildSingleEntrypoint({ cwd, entryFile }));
|
|
151
|
+
return { bundles };
|
|
152
|
+
}
|
|
153
|
+
|
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const SCRIPT_SOURCE = fileURLToPath(new URL('../workflows/build/build-scripts.js', import.meta.url));
|
|
6
|
-
const SCRIPT_TARGET = '.climaybe/build-scripts.js';
|
|
7
|
-
const SCRIPT_SHIM_TARGET = 'build-scripts.js';
|
|
8
|
-
const SCRIPT_SHIM_CONTENT = `// Auto-generated by climaybe. Keep for npm scripts compatibility.
|
|
9
|
-
// Delegates to the bundled implementation in .climaybe/.
|
|
10
|
-
const { buildScripts } = require('./.climaybe/build-scripts.js');
|
|
11
|
-
|
|
12
|
-
if (require.main === module) {
|
|
13
|
-
buildScripts();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
module.exports = { buildScripts };
|
|
17
|
-
`;
|
|
18
3
|
|
|
19
4
|
const REQUIRED_PATHS = [
|
|
20
5
|
{ path: '_scripts/main.js', kind: 'file' },
|
|
@@ -25,32 +10,6 @@ const DEFAULTS = [
|
|
|
25
10
|
{ path: 'assets', kind: 'dir' },
|
|
26
11
|
];
|
|
27
12
|
|
|
28
|
-
function targetScriptPath(cwd = process.cwd()) {
|
|
29
|
-
return join(cwd, SCRIPT_TARGET);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function installBuildScript(cwd = process.cwd()) {
|
|
33
|
-
const target = targetScriptPath(cwd);
|
|
34
|
-
mkdirSync(join(cwd, '.climaybe'), { recursive: true });
|
|
35
|
-
copyFileSync(SCRIPT_SOURCE, target);
|
|
36
|
-
const shimTarget = join(cwd, SCRIPT_SHIM_TARGET);
|
|
37
|
-
if (!existsSync(shimTarget)) {
|
|
38
|
-
writeFileSync(shimTarget, SCRIPT_SHIM_CONTENT, 'utf-8');
|
|
39
|
-
}
|
|
40
|
-
return target;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function removeBuildScript(cwd = process.cwd()) {
|
|
44
|
-
const target = targetScriptPath(cwd);
|
|
45
|
-
if (existsSync(target)) rmSync(target);
|
|
46
|
-
const shimTarget = join(cwd, SCRIPT_SHIM_TARGET);
|
|
47
|
-
if (!existsSync(shimTarget)) return;
|
|
48
|
-
const content = readFileSync(shimTarget, 'utf-8');
|
|
49
|
-
if (content === SCRIPT_SHIM_CONTENT) {
|
|
50
|
-
rmSync(shimTarget);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
13
|
export function getMissingBuildWorkflowRequirements(cwd = process.cwd()) {
|
|
55
14
|
const missing = [];
|
|
56
15
|
for (const req of REQUIRED_PATHS) {
|
|
@@ -75,5 +34,6 @@ export function ensureBuildWorkflowDefaults(cwd = process.cwd()) {
|
|
|
75
34
|
}
|
|
76
35
|
|
|
77
36
|
export function getBuildScriptRelativePath() {
|
|
78
|
-
|
|
37
|
+
// Legacy; build-scripts are now part of climaybe runtime.
|
|
38
|
+
return 'n/a';
|
|
79
39
|
}
|
package/src/lib/config.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
3
|
import { getLatestTagVersion } from './git.js';
|
|
4
4
|
|
|
5
5
|
const PKG = 'package.json';
|
|
6
|
+
const CLIMAYBE_CONFIG = 'climaybe.config.json';
|
|
7
|
+
const LEGACY_CLIMAYBE_DIR = '.climaybe';
|
|
8
|
+
const LEGACY_CLIMAYBE_CONFIG = 'config.json';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve absolute path to root climaybe config file.
|
|
12
|
+
*/
|
|
13
|
+
function climaybeConfigPath(cwd = process.cwd()) {
|
|
14
|
+
return join(cwd, CLIMAYBE_CONFIG);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function legacyClimaybeConfigPath(cwd = process.cwd()) {
|
|
18
|
+
return join(cwd, LEGACY_CLIMAYBE_DIR, LEGACY_CLIMAYBE_CONFIG);
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* Resolve absolute path to the target repo's package.json.
|
|
@@ -12,6 +26,15 @@ function pkgPath(cwd = process.cwd()) {
|
|
|
12
26
|
return join(cwd, PKG);
|
|
13
27
|
}
|
|
14
28
|
|
|
29
|
+
function readJson(path) {
|
|
30
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeJson(path, obj) {
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
writeFileSync(path, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
/**
|
|
16
39
|
* Read the full package.json from a target repo.
|
|
17
40
|
* Returns null if it doesn't exist.
|
|
@@ -20,21 +43,46 @@ function pkgPath(cwd = process.cwd()) {
|
|
|
20
43
|
export function readPkg(cwd = process.cwd()) {
|
|
21
44
|
const p = pkgPath(cwd);
|
|
22
45
|
if (!existsSync(p)) return null;
|
|
23
|
-
return
|
|
46
|
+
return readJson(p);
|
|
24
47
|
}
|
|
25
48
|
|
|
26
49
|
/**
|
|
27
50
|
* Write the full package.json object back to disk.
|
|
28
51
|
*/
|
|
29
52
|
export function writePkg(pkg, cwd = process.cwd()) {
|
|
30
|
-
|
|
53
|
+
writeJson(pkgPath(cwd), pkg);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read the climaybe config from root file (with legacy fallback).
|
|
58
|
+
* Returns null if neither exists.
|
|
59
|
+
*/
|
|
60
|
+
export function readClimaybeConfig(cwd = process.cwd()) {
|
|
61
|
+
const p = climaybeConfigPath(cwd);
|
|
62
|
+
if (existsSync(p)) return readJson(p);
|
|
63
|
+
const legacy = legacyClimaybeConfigPath(cwd);
|
|
64
|
+
if (existsSync(legacy)) return readJson(legacy);
|
|
65
|
+
return null;
|
|
31
66
|
}
|
|
32
67
|
|
|
33
68
|
/**
|
|
34
|
-
*
|
|
35
|
-
|
|
69
|
+
* Write/merge climaybe config into root config file.
|
|
70
|
+
*/
|
|
71
|
+
export function writeClimaybeConfig(config, cwd = process.cwd()) {
|
|
72
|
+
const current = readClimaybeConfig(cwd) || {};
|
|
73
|
+
const next = { ...current, ...config };
|
|
74
|
+
writeJson(climaybeConfigPath(cwd), next);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read the climaybe config (source of truth).
|
|
79
|
+
* - Primary: climaybe.config.json
|
|
80
|
+
* - Back-compat: .climaybe/config.json
|
|
81
|
+
* - Legacy fallback: package.json → config
|
|
36
82
|
*/
|
|
37
83
|
export function readConfig(cwd = process.cwd()) {
|
|
84
|
+
const cfg = readClimaybeConfig(cwd);
|
|
85
|
+
if (cfg) return cfg;
|
|
38
86
|
const pkg = readPkg(cwd);
|
|
39
87
|
return pkg?.config ?? null;
|
|
40
88
|
}
|
|
@@ -42,17 +90,24 @@ export function readConfig(cwd = process.cwd()) {
|
|
|
42
90
|
/**
|
|
43
91
|
* @typedef {object} WriteConfigOptions
|
|
44
92
|
* @property {string} [defaultPackageName] - When creating package.json, set `name` (default: shopify-theme).
|
|
93
|
+
* @property {boolean} [alsoWriteLegacyPackageConfig] - Also write package.json config (default: false).
|
|
45
94
|
*/
|
|
46
95
|
|
|
47
96
|
/**
|
|
48
|
-
* Write/merge the climaybe config
|
|
49
|
-
*
|
|
97
|
+
* Write/merge the climaybe config (source of truth: climaybe.config.json).
|
|
98
|
+
* Optionally writes legacy package.json config for transitional repos.
|
|
50
99
|
* @param {object} config
|
|
51
100
|
* @param {string} [cwd]
|
|
52
101
|
* @param {WriteConfigOptions} [options]
|
|
53
102
|
*/
|
|
54
103
|
export function writeConfig(config, cwd = process.cwd(), options = {}) {
|
|
55
104
|
const defaultPackageName = options.defaultPackageName ?? 'shopify-theme';
|
|
105
|
+
const alsoWriteLegacyPackageConfig = options.alsoWriteLegacyPackageConfig ?? false;
|
|
106
|
+
|
|
107
|
+
writeClimaybeConfig(config, cwd);
|
|
108
|
+
|
|
109
|
+
if (!alsoWriteLegacyPackageConfig) return;
|
|
110
|
+
|
|
56
111
|
let pkg = readPkg(cwd);
|
|
57
112
|
if (!pkg) {
|
|
58
113
|
let version = '1.0.0';
|
|
@@ -73,6 +128,21 @@ export function writeConfig(config, cwd = process.cwd(), options = {}) {
|
|
|
73
128
|
writePkg(pkg, cwd);
|
|
74
129
|
}
|
|
75
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Migrate legacy package.json config into climaybe.config.json.
|
|
133
|
+
* Returns true if migration wrote a config file.
|
|
134
|
+
*/
|
|
135
|
+
export function migrateLegacyPackageConfigToClimaybe({ cwd = process.cwd(), overwrite = false } = {}) {
|
|
136
|
+
const legacy = readPkg(cwd)?.config ?? null;
|
|
137
|
+
if (!legacy || typeof legacy !== 'object') return false;
|
|
138
|
+
|
|
139
|
+
const existing = readClimaybeConfig(cwd);
|
|
140
|
+
if (existing && !overwrite) return false;
|
|
141
|
+
|
|
142
|
+
writeJson(climaybeConfigPath(cwd), legacy);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
76
146
|
/**
|
|
77
147
|
* Resolved project kind for guards and init flows.
|
|
78
148
|
* - `app` only when config explicitly sets project_type: app.
|