ylyx-cli 1.0.15 → 1.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/bin/ylyx.js CHANGED
@@ -4,7 +4,7 @@ const { program } = require('commander');
4
4
  const { generate } = require('../lib/index');
5
5
  const { listTemplates, getTemplateInfo, addTemplate } = require('../lib/template');
6
6
  const { interactiveCreate } = require('../lib/interactive');
7
- const { setMode, initConfig } = require('../lib/config');
7
+ const { setEnv, initConfig, migrateConfig, chooseEnvInteractive } = require('../lib/config');
8
8
  const { deploy } = require('../lib/deploy');
9
9
  const pkg = require('../package.json');
10
10
 
@@ -18,21 +18,15 @@ program
18
18
  });
19
19
 
20
20
  program
21
- .command('config [mode]')
22
- .description('写入/更新 .ylyxrc.json 配置(目前支持:mode=dev|prod)')
23
- .option('-m, --mode <mode>', '运行模式:dev | prod')
24
- .option('--symlink', '切换 default.js 时强制使用 symlink(dev 默认)')
25
- .option('--copy', '切换 default.js 时强制使用 copy')
26
- .action((modeArg, options) => {
21
+ .command('config [name]')
22
+ .description('写入/更新 .ylyxrc.json 配置(切换到指定环境;不传参数时交互式选择)')
23
+ .action(async (name) => {
27
24
  try {
28
- const mode = options.mode || modeArg;
29
- if (!mode) {
30
- console.error('❌ 缺少参数:mode(dev|prod)');
31
- console.error('示例:ylyx config --mode dev 或 ylyx config dev');
32
- process.exit(1);
25
+ if (name) {
26
+ setEnv(name);
27
+ } else {
28
+ await chooseEnvInteractive();
33
29
  }
34
- const method = options.copy ? 'copy' : options.symlink ? 'symlink' : undefined;
35
- setMode(mode, { method });
36
30
  } catch (error) {
37
31
  console.error('❌ 配置失败:', error.message);
38
32
  process.exit(1);
@@ -44,7 +38,7 @@ program
44
38
  .description('初始化相关资源')
45
39
  .command('config')
46
40
  .description('初始化配置(生成 .ylyxrc.json / config/default-*.js 等)')
47
- .option('-m, --mode <mode>', '初始 mode:dev | prod(默认 dev)')
41
+ .option('-e, --env <name>', '初始 currentEnv(默认 dev)')
48
42
  .option('-f, --force', '强制覆盖已存在文件')
49
43
  .action((options) => {
50
44
  try {
@@ -55,6 +49,20 @@ program
55
49
  }
56
50
  });
57
51
 
52
+ program
53
+ .command('migrate')
54
+ .description('迁移相关资源')
55
+ .command('config')
56
+ .description('迁移旧版 .ylyxrc.json(mode/deploy)到 environments 结构')
57
+ .action(() => {
58
+ try {
59
+ migrateConfig();
60
+ } catch (error) {
61
+ console.error('❌ 迁移失败:', error.message);
62
+ process.exit(1);
63
+ }
64
+ });
65
+
58
66
  program
59
67
  .command('generate <template-name>')
60
68
  .alias('g')
@@ -113,15 +121,15 @@ program
113
121
 
114
122
  program
115
123
  .command('deploy')
116
- .description('部署:递归上传本地目录到远端目录(读取当前项目 .ylyxrc.json 的 deploy 字段)')
117
- .option('--env <env>', '部署环境:test|prod(不传会交互选择)')
118
- .option('--build-dir <name>', '压缩文件名使用的 buildDir(默认取 .ylyxrc.json 的 buildDir)')
119
- .option('--host <host>', '覆盖 deploy.host')
120
- .option('--port <port>', '覆盖 deploy.port')
121
- .option('-u, --username <username>', '覆盖 deploy.username')
122
- .option('-p, --password <password>', '覆盖 deploy.password')
123
- .option('--local <path>', '覆盖 deploy.localDir(默认 ./EXTERNAL_DIGIC)')
124
- .option('--remote <path>', '覆盖 deploy.remoteDir')
124
+ .description('部署:递归上传本地目录到远端目录(读取当前项目 .ylyxrc.json 的 environments 数组)')
125
+ .option('--env <name>', '部署环境名称(不传会交互选择)')
126
+ .option('--build-dir <name>', '压缩文件名使用的 buildDir(默认取该环境的 buildDir)')
127
+ .option('--host <host>', '覆盖该环境的 host')
128
+ .option('--port <port>', '覆盖该环境的 port')
129
+ .option('-u, --username <username>', '覆盖该环境的 username')
130
+ .option('-p, --password <password>', '覆盖该环境的 password')
131
+ .option('--local <path>', '覆盖该环境的 localDir(默认取该环境的 buildDir)')
132
+ .option('--remote <path>', '覆盖该环境的 remoteDir')
125
133
  .option('--zip', '部署后把 localDir 压缩成 zip(参考 compressing.zip.compressDir)')
126
134
  .option('--zip-out <path>', 'zip 输出目录(默认:./.ylyx-deploy)')
127
135
  .action(async (options) => {
package/lib/config.js CHANGED
@@ -1,77 +1,15 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
+ const inquirer = require('inquirer');
3
4
  const { log } = require('./utils');
4
-
5
- function getConfigPath() {
6
- return path.join(process.cwd(), '.ylyxrc.json');
7
- }
8
-
9
- function normalizeMode(mode) {
10
- if (typeof mode !== 'string') return null;
11
- const m = mode.trim().toLowerCase();
12
- if (m === 'dev' || m === 'prod') return m;
13
- return null;
14
- }
15
-
16
- function parseDotEnvFile(envPath) {
17
- if (!fs.existsSync(envPath)) return {};
18
- const content = fs.readFileSync(envPath, 'utf-8');
19
- const lines = content.split(/\r?\n/);
20
- const out = {};
21
- for (const raw of lines) {
22
- const line = raw.trim();
23
- if (!line || line.startsWith('#')) continue;
24
- const idx = line.indexOf('=');
25
- if (idx < 0) continue;
26
- const key = line.slice(0, idx).trim();
27
- let value = line.slice(idx + 1).trim();
28
- if (
29
- (value.startsWith('"') && value.endsWith('"')) ||
30
- (value.startsWith("'") && value.endsWith("'"))
31
- ) {
32
- value = value.slice(1, -1);
33
- }
34
- out[key] = value;
35
- }
36
- return out;
37
- }
38
-
39
- function normalizePublicUrlToDir(publicUrl) {
40
- if (!publicUrl || typeof publicUrl !== 'string') return '';
41
- let v = publicUrl.trim();
42
- v = v.replace(/\\/g, '/');
43
- // ignore full urls
44
- if (/^https?:\/\//i.test(v)) return '';
45
- v = v.replace(/^\/+/, '').replace(/\/+$/, '');
46
- return v;
47
- }
48
-
49
- function resolveProdBuildDir(config) {
50
- // 1) 优先使用 .ylyxrc.json 的 buildDir(可为相对/绝对路径)
51
- if (config.buildDir) {
52
- return path.resolve(process.cwd(), config.buildDir);
53
- }
54
-
55
- // 2) 默认:读取 .env.production 的 VUE_APP_PUBLIC_URL,拼成 <publicUrlDir>
56
- const envProdPath = path.join(process.cwd(), '.env.production');
57
- const env = parseDotEnvFile(envProdPath);
58
- const publicUrlDir = normalizePublicUrlToDir(env.VUE_APP_PUBLIC_URL);
59
- if (publicUrlDir) {
60
- return path.resolve(process.cwd(), publicUrlDir);
61
- }
62
- throw new Error(
63
- '无法确定 prod 打包输出目录:请在 .ylyxrc.json 中配置 buildDir,或在 .env.production 中配置 VUE_APP_PUBLIC_URL'
64
- );
65
- }
66
-
67
- function readConfigSafe(configPath) {
68
- if (!fs.existsSync(configPath)) return {};
69
- try {
70
- return fs.readJsonSync(configPath);
71
- } catch (e) {
72
- throw new Error(`读取 .ylyxrc.json 失败,请确认它是有效 JSON:${e.message}`);
73
- }
74
- }
5
+ const {
6
+ getConfigPath,
7
+ readConfigSafe,
8
+ assertConfigMigrated,
9
+ findEnvironment,
10
+ buildMigratedConfig,
11
+ CONFIG_VERSION,
12
+ } = require('./environments');
75
13
 
76
14
  function writeIfAllowed(filePath, content, force) {
77
15
  if (fs.existsSync(filePath) && !force) {
@@ -84,12 +22,23 @@ function writeIfAllowed(filePath, content, force) {
84
22
  return true;
85
23
  }
86
24
 
87
- function deriveDefaultVariantContent(baseContent, mode) {
88
- const isDev = mode === 'dev';
25
+ /**
26
+ * 生成 config/default-<envName>.js 的内容:
27
+ * 注入 `let env = '<envName>';` 与 `let isDev = (envName === 'dev');`(向后兼容旧代码)
28
+ */
29
+ function deriveDefaultVariantContent(baseContent, envName) {
30
+ const isDev = envName === 'dev';
89
31
 
90
32
  let content = baseContent || '';
33
+ const envRe = /let\s+env\s*=\s*[^;]*;/;
91
34
  const devRe = /let\s+isDev\s*=\s*[^;]*;/;
92
35
 
36
+ if (envRe.test(content)) {
37
+ content = content.replace(envRe, `let env = '${envName}';`);
38
+ } else {
39
+ content = `let env = '${envName}';\n` + content;
40
+ }
41
+
93
42
  if (devRe.test(content)) {
94
43
  content = content.replace(devRe, `let isDev = ${isDev};`);
95
44
  } else {
@@ -100,127 +49,201 @@ function deriveDefaultVariantContent(baseContent, mode) {
100
49
  // 删除历史遗留的 isProd 变量与输出字段(若存在)
101
50
  content = content.replace(/let\s+isProd\s*=\s*[^;]*;\s*\r?\n?/g, '');
102
51
  content = content.replace(/window\.config\.IS_PROD\s*=\s*[^;]*;\s*\r?\n?/g, '');
103
- // 删除对象形式的 IS_PROD 字段(例如 IS_PROD: isProd,)
104
52
  content = content.replace(/^\s*IS_PROD\s*:\s*[^,}]*,?\s*\r?\n?/gm, '');
105
53
 
106
54
  return content;
107
55
  }
108
56
 
109
- function ensureSymlinkOrCopy({ src, dest, preferSymlink }) {
57
+ function copyFile({ src, dest }) {
110
58
  fs.ensureDirSync(path.dirname(dest));
111
59
  if (!fs.existsSync(src)) {
112
60
  log.warn(`未找到源文件:${path.relative(process.cwd(), src)}`);
113
61
  return { ok: false, method: 'missing' };
114
62
  }
115
63
 
116
- // 如果目标已存在,先删除(不管是文件还是链接)
117
64
  if (fs.existsSync(dest)) {
118
65
  fs.removeSync(dest);
119
66
  }
120
67
 
121
- if (preferSymlink) {
122
- try {
123
- // Windows/macOS/Linux 都支持;Windows 可能因为权限失败,失败后回退 copy
124
- fs.ensureSymlinkSync(src, dest, 'file');
125
- return { ok: true, method: 'symlink' };
126
- } catch (e) {
127
- log.warn(`创建 symlink 失败,将降级为 copy:${e.message}`);
128
- }
129
- }
130
-
131
68
  try {
132
69
  fs.copyFileSync(src, dest);
133
- return { ok: true, method: 'copy' };
70
+ return { ok: true };
134
71
  } catch (e) {
135
72
  log.error(`写入失败:${e.message}`);
136
- return { ok: false, method: 'copy_failed' };
73
+ return { ok: false };
137
74
  }
138
75
  }
139
76
 
140
77
  /**
141
- * 写入/更新 .ylyxrc.json 的 mode 字段(dev|prod),并与已有配置合并
78
+ * 切换到指定环境:把 config/default-<name>.js 复制到该环境的目标位置
79
+ * - isLocal 环境 -> publicDir/default.js
80
+ * - 其他环境 -> <环境 buildDir>/default.js
142
81
  */
143
- function setMode(mode, options = {}) {
144
- const normalized = normalizeMode(mode);
145
- if (!normalized) {
146
- throw new Error(`mode 取值只能是 dev 或 prod(当前: ${mode})`);
147
- }
148
-
82
+ function setEnv(name) {
149
83
  const configPath = getConfigPath();
84
+ const configExists = fs.existsSync(configPath);
150
85
  const config = readConfigSafe(configPath);
86
+ assertConfigMigrated(config, configExists);
87
+
88
+ const env = findEnvironment(config, name);
151
89
 
152
- // 可选:如果当前项目目录存在 public/default-{mode}.js,则复制到 public/default.js
153
- // 注意:切换应发生在“项目目录”(process.cwd()),而不是 CLI 自身的 lib 目录(__dirname)
154
- // 优先从 .ylyxrc.json 中读取 publicDir,其次使用默认 ./public
155
90
  const configDir = path.resolve(process.cwd(), config.configDir || 'config');
156
91
  const publicDir = path.resolve(process.cwd(), config.publicDir || 'public');
157
- const defaultVariant = path.join(configDir, `default-${normalized}.js`);
158
- const defaultFile =
159
- normalized === 'prod'
160
- ? path.join(resolveProdBuildDir(config), 'default.js')
161
- : path.join(publicDir, 'default.js');
162
-
163
- // 默认策略:dev 使用 symlink(实时同步),prod 使用 copy(稳定)
164
- const method = options.method || (normalized === 'dev' ? 'symlink' : 'copy');
165
- const result = ensureSymlinkOrCopy({
166
- src: defaultVariant,
167
- dest: defaultFile,
168
- preferSymlink: method === 'symlink',
169
- });
92
+ const defaultVariant = path.join(configDir, `default-${env.name}.js`);
93
+
94
+ // ponytail: 信任本地 .ylyxrc.json 的路径配置,不做路径穿越校验(本地 CLI 工具,配置由用户自行维护)
95
+ let targetDir;
96
+ if (env.isLocal) {
97
+ targetDir = publicDir;
98
+ } else {
99
+ if (!env.buildDir) {
100
+ throw new Error(`环境 "${env.name}" 未配置 buildDir,无法确定切换目标目录`);
101
+ }
102
+ targetDir = path.resolve(process.cwd(), env.buildDir);
103
+ }
104
+ const defaultFile = path.join(targetDir, 'default.js');
105
+
106
+ const result = copyFile({ src: defaultVariant, dest: defaultFile });
170
107
 
171
108
  if (result.ok) {
172
- const label = result.method === 'symlink' ? 'symlink' : 'copy';
173
109
  log.success(
174
- `已(${label})切换 ${path.relative(process.cwd(), defaultFile)} <- ${path.relative(
110
+ `已切换 ${path.relative(process.cwd(), defaultFile)} <- ${path.relative(
175
111
  process.cwd(),
176
112
  defaultVariant
177
113
  )}`
178
114
  );
179
- if (normalized === 'dev' && result.method === 'symlink') {
180
- log.info('dev 模式实时同步已启用:修改 default-dev.js 会立即反映到 default.js');
181
- }
182
- } else if (result.method === 'missing') {
115
+ } else if (!result.ok && !fs.existsSync(defaultVariant)) {
183
116
  log.warn(`未找到 ${path.relative(process.cwd(), defaultVariant)},请先运行 ylyx init config`);
184
117
  }
185
118
 
186
- config.mode = normalized;
119
+ config.currentEnv = env.name;
187
120
  fs.writeJsonSync(configPath, config, { spaces: 2 });
188
- log.success(`已写入配置:mode=${normalized}`);
121
+ log.success(`已写入配置:currentEnv=${env.name}`);
189
122
  log.info(`配置文件:${configPath}`);
190
123
  }
191
124
 
125
+ /**
126
+ * 交互式选择环境并切换:按 isLocal 分组展示,当前环境标注"(当前)",
127
+ * 默认光标停在当前环境,选定后复用 setEnv 完成实际切换
128
+ */
129
+ async function chooseEnvInteractive() {
130
+ const configPath = getConfigPath();
131
+ const configExists = fs.existsSync(configPath);
132
+ const config = readConfigSafe(configPath);
133
+ assertConfigMigrated(config, configExists);
134
+
135
+ const environments = config.environments || [];
136
+ if (environments.length === 0) {
137
+ throw new Error('环境列表为空,请先在 .ylyxrc.json 配置 environments 或运行 ylyx init config');
138
+ }
139
+
140
+ const currentEnv = config.currentEnv;
141
+ const toChoice = (e) => ({
142
+ name: e.name === currentEnv ? `${e.name}(当前)` : e.name,
143
+ value: e.name,
144
+ });
145
+
146
+ const localEnvs = environments.filter((e) => e.isLocal);
147
+ const remoteEnvs = environments.filter((e) => !e.isLocal);
148
+
149
+ const choices = [];
150
+ if (localEnvs.length > 0) {
151
+ choices.push(new inquirer.Separator('-- 本地环境 --'));
152
+ choices.push(...localEnvs.map(toChoice));
153
+ }
154
+ if (remoteEnvs.length > 0) {
155
+ choices.push(new inquirer.Separator('-- 部署环境 --'));
156
+ choices.push(...remoteEnvs.map(toChoice));
157
+ }
158
+
159
+ const ans = await inquirer.prompt([
160
+ {
161
+ type: 'list',
162
+ name: 'env',
163
+ message: '请选择要切换到的环境',
164
+ choices,
165
+ default: currentEnv,
166
+ },
167
+ ]);
168
+
169
+ setEnv(ans.env);
170
+ }
171
+
172
+ /**
173
+ * 迁移旧版 .ylyxrc.json(mode/deploy/顶层 buildDir/preScripts)到 environments 结构
174
+ */
175
+ function migrateConfig() {
176
+ const configPath = getConfigPath();
177
+ const existing = readConfigSafe(configPath);
178
+
179
+ if (typeof existing.configVersion === 'number' && existing.configVersion >= CONFIG_VERSION) {
180
+ log.info(`当前配置已是最新版本(configVersion=${existing.configVersion}),无需迁移`);
181
+ return;
182
+ }
183
+
184
+ const migrated = buildMigratedConfig(existing);
185
+ fs.writeJsonSync(configPath, migrated, { spaces: 2 });
186
+
187
+ const names = migrated.environments.map((e) => e.name).join(', ');
188
+ log.success(`迁移完成,已生成环境:${names}`);
189
+ log.warn('请检查 test/prod 环境的 host/username/password 等敏感信息是否正确');
190
+ log.info('请运行 `ylyx init config --force` 以补写 package.json 的 pre/post 钩子(迁移命令不会修改 package.json)');
191
+ }
192
+
192
193
  /**
193
194
  * init config:初始化配置目录与示例文件
194
- * - 写入/补齐 .ylyxrc.json(mode/publicDir/configDir)
195
- * - 生成 config/default-dev.js 与 config/default-prod.js(基于现有 public/default.js 派生)
196
- * - 若 public/default.js 不存在,则按当前 mode 生成一份
195
+ * - 写入/补齐 .ylyxrc.json(configVersion/environments/currentEnv/publicDir/configDir)
196
+ * - 为每个环境生成 config/default-<name>.js(基于现有 public/default.js 派生)
197
+ * - 若 public/default.js 不存在,则按 currentEnv 生成一份
198
+ * - 按每个环境的 npmScript 写 package.json 的 pre/post 钩子
197
199
  */
198
200
  function initConfig(options = {}) {
199
201
  const force = !!options.force;
200
202
  const configPath = getConfigPath();
201
203
  const existing = readConfigSafe(configPath);
202
204
 
205
+ const hasEnvironments = Array.isArray(existing.environments) && existing.environments.length > 0;
206
+ if (Array.isArray(existing.environments) && existing.environments.length === 0) {
207
+ log.info('检测到 .ylyxrc.json 中 environments 为空数组,已使用默认环境(dev/prod)填充');
208
+ }
209
+ const environments = hasEnvironments
210
+ ? existing.environments
211
+ : [
212
+ { name: 'dev', isLocal: true, npmScript: 'dev', deployMethod: 'none' },
213
+ {
214
+ name: 'prod',
215
+ npmScript: 'build:prod',
216
+ deployMethod: 'zip',
217
+ buildDir: 'dist/app',
218
+ zipOutDir: './.ylyx-deploy',
219
+ },
220
+ ];
221
+
222
+ let currentEnv = existing.currentEnv || 'dev';
223
+ if (options.env) {
224
+ const exists = environments.some((e) => e.name === options.env);
225
+ if (!exists) {
226
+ throw new Error(
227
+ `环境不存在: ${options.env}\n可用环境: ${environments.map((e) => e.name).join(', ')}`
228
+ );
229
+ }
230
+ currentEnv = options.env;
231
+ }
232
+
203
233
  const next = {
204
234
  ...existing,
205
- mode: existing.mode || 'dev',
235
+ configVersion: CONFIG_VERSION,
206
236
  publicDir: existing.publicDir || './public',
207
237
  configDir: existing.configDir || './config',
208
- // npm scripts:默认对 dev 与 build:prod 增加前置脚本 pre<name>
209
- // 可在 .ylyxrc.json 中改成你的项目脚本名(例如 dev=serve,prod=build)
210
- preScripts: existing.preScripts || { dev: 'dev', prod: 'build:prod' },
238
+ currentEnv,
239
+ environments,
211
240
  };
212
241
 
213
- if (options.mode) {
214
- const normalized = normalizeMode(options.mode);
215
- if (!normalized) throw new Error(`mode 取值只能是 dev 或 prod(当前: ${options.mode})`);
216
- next.mode = normalized;
217
- }
218
-
219
242
  fs.writeJsonSync(configPath, next, { spaces: 2 });
220
243
  log.success(`已初始化配置文件:${path.relative(process.cwd(), configPath)}`);
221
244
 
222
- const publicDirAbs = path.resolve(process.cwd(), next.publicDir || 'public');
223
- const configDirAbs = path.resolve(process.cwd(), next.configDir || 'config');
245
+ const publicDirAbs = path.resolve(process.cwd(), next.publicDir);
246
+ const configDirAbs = path.resolve(process.cwd(), next.configDir);
224
247
  fs.ensureDirSync(publicDirAbs);
225
248
  fs.ensureDirSync(configDirAbs);
226
249
 
@@ -229,23 +252,16 @@ function initConfig(options = {}) {
229
252
  if (fs.existsSync(publicDefault)) {
230
253
  baseContent = fs.readFileSync(publicDefault, 'utf-8');
231
254
  } else {
232
- baseContent =
233
- "// 自动生成的默认配置(请按项目需要补全)\n" +
234
- "let isDev = true;\n"
255
+ baseContent = '// 自动生成的默认配置(请按项目需要补全)\n';
235
256
  }
236
257
 
237
- const devVariantPath = path.join(configDirAbs, 'default-dev.js');
238
- const prodVariantPath = path.join(configDirAbs, 'default-prod.js');
239
-
240
- writeIfAllowed(devVariantPath, deriveDefaultVariantContent(baseContent, 'dev'), force);
241
- writeIfAllowed(prodVariantPath, deriveDefaultVariantContent(baseContent, 'prod'), force);
258
+ for (const env of next.environments) {
259
+ const variantPath = path.join(configDirAbs, `default-${env.name}.js`);
260
+ writeIfAllowed(variantPath, deriveDefaultVariantContent(baseContent, env.name), force);
261
+ }
242
262
 
243
- // 如果 public/default.js 不存在,则按当前 mode 生成一份
244
263
  if (!fs.existsSync(publicDefault)) {
245
- const src =
246
- next.mode === 'prod'
247
- ? path.join(configDirAbs, 'default-prod.js')
248
- : path.join(configDirAbs, 'default-dev.js');
264
+ const src = path.join(configDirAbs, `default-${next.currentEnv}.js`);
249
265
  if (fs.existsSync(src)) {
250
266
  fs.copyFileSync(src, publicDefault);
251
267
  log.success(
@@ -256,10 +272,9 @@ function initConfig(options = {}) {
256
272
  log.info(`已存在:${path.relative(process.cwd(), publicDefault)}(未覆盖)`);
257
273
  }
258
274
 
259
- // 初始化 package.json 的 npm 前置脚本:pre<devScript> / pre<prodScript>
260
275
  const packageJsonPath = path.join(process.cwd(), 'package.json');
261
276
  if (!fs.existsSync(packageJsonPath)) {
262
- log.warn('未找到 package.json,已跳过 npm 前置脚本初始化');
277
+ log.warn('未找到 package.json,已跳过 npm 钩子初始化');
263
278
  return;
264
279
  }
265
280
 
@@ -271,21 +286,30 @@ function initConfig(options = {}) {
271
286
  }
272
287
 
273
288
  pkg.scripts = pkg.scripts || {};
274
- const devTarget = (next.preScripts && next.preScripts.dev) || 'dev';
275
- const prodTarget = (next.preScripts && next.preScripts.prod) || 'build:prod';
276
289
 
277
- const desired = [
278
- { name: `pre${devTarget}`, value: 'ylyx config dev' },
279
- { name: `post${prodTarget}`, value: 'ylyx config prod && ylyx deploy' },
280
- ];
290
+ for (const env of next.environments) {
291
+ const npmScript = env.npmScript;
292
+ if (!npmScript) continue;
293
+
294
+ let hookName;
295
+ let hookValue;
296
+ if (env.isLocal) {
297
+ hookName = `pre${npmScript}`;
298
+ hookValue = `ylyx config ${env.name}`;
299
+ } else {
300
+ hookName = `post${npmScript}`;
301
+ hookValue =
302
+ env.deployMethod && env.deployMethod !== 'none'
303
+ ? `ylyx config ${env.name} && ylyx deploy --env ${env.name}`
304
+ : `ylyx config ${env.name}`;
305
+ }
281
306
 
282
- for (const item of desired) {
283
- if (pkg.scripts[item.name] && !force) {
284
- log.warn(`package.json scripts 已存在,跳过:${item.name}`);
307
+ if (pkg.scripts[hookName] && !force) {
308
+ log.warn(`package.json scripts 已存在,跳过:${hookName}`);
285
309
  continue;
286
310
  }
287
- pkg.scripts[item.name] = item.value;
288
- log.success(`已写入 package.json scripts:${item.name}`);
311
+ pkg.scripts[hookName] = hookValue;
312
+ log.success(`已写入 package.json scripts:${hookName}`);
289
313
  }
290
314
 
291
315
  fs.writeJsonSync(packageJsonPath, pkg, { spaces: 2 });
@@ -293,7 +317,9 @@ function initConfig(options = {}) {
293
317
  }
294
318
 
295
319
  module.exports = {
296
- setMode,
297
- normalizeMode,
320
+ setEnv,
298
321
  initConfig,
322
+ migrateConfig,
323
+ chooseEnvInteractive,
324
+ deriveDefaultVariantContent,
299
325
  };