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/lib/deploy.js CHANGED
@@ -4,6 +4,7 @@ const ora = require('ora');
4
4
  const inquirer = require('inquirer');
5
5
  const archiver = require('archiver');
6
6
  const { readProjectConfig } = require('./utils');
7
+ const { assertConfigMigrated, findEnvironment, getConfigPath } = require('./environments');
7
8
 
8
9
  let spinner = null; // 加载实例
9
10
 
@@ -123,6 +124,40 @@ function getBuildDirBaseName({ buildDir, localDir }) {
123
124
  return sanitizeBuildDirName(path.basename(path.resolve(localDir || process.cwd())));
124
125
  }
125
126
 
127
+ function normalizeAndValidateRemoteDir(remoteDir) {
128
+ if (!remoteDir || typeof remoteDir !== 'string') return null;
129
+ // 统一成 Linux 风格
130
+ const raw = remoteDir.trim().replace(/\\/g, '/');
131
+ // 必须是绝对路径
132
+ if (!raw.startsWith('/')) {
133
+ throw new Error(
134
+ `deploy.remoteDir 必须是 Linux 绝对路径,并且只能在 /usr/local/nginx/html/* 下(当前: ${remoteDir})`
135
+ );
136
+ }
137
+
138
+ // 归一化,消除 .. 等
139
+ const resolved = path.posix.resolve(raw);
140
+ const base = '/usr/local/nginx/html';
141
+ const baseWithSlash = base + '/';
142
+
143
+ // 只允许 /usr/local/nginx/html/*(必须是子目录,不允许刚好等于 base)
144
+ if (!resolved.startsWith(baseWithSlash)) {
145
+ throw new Error(
146
+ `deploy.remoteDir 只能设置为 ${baseWithSlash}* 下的目录(当前: ${resolved})`
147
+ );
148
+ }
149
+ if (resolved === base) {
150
+ throw new Error(`deploy.remoteDir 不能是 ${base} 根目录,请设置为其子目录(例如 ${baseWithSlash}my-app)`);
151
+ }
152
+ // 再防一次:禁止路径段包含 ..(即便 resolve 已处理,这里保证输入约束更明确)
153
+ const rel = resolved.slice(baseWithSlash.length);
154
+ if (!rel || rel.split('/').some((seg) => seg === '..')) {
155
+ throw new Error(`deploy.remoteDir 非法:不允许包含 ..(当前: ${resolved})`);
156
+ }
157
+
158
+ return resolved;
159
+ }
160
+
126
161
  async function zipDirAsTopFolder({ srcDir, zipPath, topFolderName }) {
127
162
  fs.ensureDirSync(path.dirname(zipPath));
128
163
  await new Promise((resolve, reject) => {
@@ -172,18 +207,34 @@ function computeDirHashLetters(localDir) {
172
207
  return hashLettersFromSha256(digest, 12);
173
208
  }
174
209
 
175
- async function chooseDeployEnv(env) {
176
- if (env === 'test' || env === 'prod') return env;
210
+ // 注意:这里"环境不存在或不可部署"的判断逻辑与 resolveDeployConfig 内部的
211
+ // findEnvironment + deployMethod==='none' 检查在语义上等价,只是分别在
212
+ // "交互选择阶段"和"最终解析阶段"各自独立校验一次(后者作为独立导出函数,
213
+ // 必须能脱离 chooseDeployEnv 单独工作,因此不合并成一个函数)。
214
+ async function chooseDeployEnv(projectConfig, envArg) {
215
+ const environments = (projectConfig.environments || []).filter((e) => e.deployMethod !== 'none');
216
+
217
+ if (envArg) {
218
+ if (!environments.some((e) => e.name === envArg)) {
219
+ const available = environments.map((e) => e.name).join(', ') || '(无)';
220
+ throw new Error(`环境 "${envArg}" 不可部署或不存在\n可部署环境: ${available}`);
221
+ }
222
+ return envArg;
223
+ }
224
+
225
+ if (environments.length === 0) {
226
+ throw new Error('没有可部署的环境(所有环境的 deployMethod 均为 none,或未定义 environments)');
227
+ }
228
+
177
229
  const ans = await inquirer.prompt([
178
230
  {
179
231
  type: 'list',
180
232
  name: 'env',
181
233
  message: '请选择部署环境',
182
- choices: [
183
- { name: '部署测试服(上传到服务器)', value: 'test' },
184
- { name: '部署正式服(不上传,仅压缩)', value: 'prod' },
185
- ],
186
- default: 'test',
234
+ choices: environments.map((e) => ({
235
+ name: `${e.name}(${e.deployMethod === 'upload' ? '上传到服务器' : '仅压缩'})`,
236
+ value: e.name,
237
+ })),
187
238
  },
188
239
  ]);
189
240
  return ans.env;
@@ -255,96 +306,60 @@ async function uploadDirWithProgress({ sftp, localDir, remoteDirPosix, onProgres
255
306
  return { totalBytes, uploadedBytes, fileCount: files.length };
256
307
  }
257
308
 
258
- function resolveDeployConfig(overrides = {}) {
259
- const projectConfig = readProjectConfig();
260
- const deploy = (projectConfig && projectConfig.deploy) || {};
261
-
262
- function parseDotEnvFile(envPath) {
263
- if (!fs.existsSync(envPath)) return {};
264
- const content = fs.readFileSync(envPath, 'utf-8');
265
- const lines = content.split(/\r?\n/);
266
- const out = {};
267
- for (const raw of lines) {
268
- const line = raw.trim();
269
- if (!line || line.startsWith('#')) continue;
270
- const idx = line.indexOf('=');
271
- if (idx < 0) continue;
272
- const key = line.slice(0, idx).trim();
273
- let value = line.slice(idx + 1).trim();
274
- if (
275
- (value.startsWith('"') && value.endsWith('"')) ||
276
- (value.startsWith("'") && value.endsWith("'"))
277
- ) {
278
- value = value.slice(1, -1);
279
- }
280
- out[key] = value;
281
- }
282
- return out;
309
+ function resolveDeployConfig(envName, overrides = {}, projectConfig) {
310
+ if (!projectConfig) {
311
+ projectConfig = readProjectConfig();
312
+ assertConfigMigrated(projectConfig, fs.existsSync(getConfigPath()));
283
313
  }
314
+ const env = findEnvironment(projectConfig, envName);
284
315
 
285
- function normalizePublicUrlToDir(publicUrl) {
286
- // config.js 规则保持一致:忽略完整 URL;去掉首尾 /;把 \ 转 /
287
- if (!publicUrl || typeof publicUrl !== 'string') return '';
288
- let v = publicUrl.trim();
289
- v = v.replace(/\\/g, '/');
290
- if (/^https?:\/\//i.test(v)) return '';
291
- v = v.replace(/^\/+/, '').replace(/\/+$/, '');
292
- return v;
316
+ if (env.deployMethod === 'none') {
317
+ throw new Error(`环境 "${env.name}" 不支持部署(deployMethod=none)`);
293
318
  }
294
319
 
295
- // localDir 默认优先从项目 .env.production 推导(VUE_APP_PUBLIC_URL -> 目录名)
296
- const envProdPath = path.join(process.cwd(), '.env.production');
297
- const envProd = parseDotEnvFile(envProdPath);
298
- const publicUrlDir = normalizePublicUrlToDir(envProd.VUE_APP_PUBLIC_URL);
299
- const inferredFromEnvProd = publicUrlDir ? path.resolve(process.cwd(), publicUrlDir) : null;
320
+ const buildDir = overrides.buildDir ?? env.buildDir;
321
+ const localDirRaw = overrides.localDir ?? env.localDir ?? buildDir;
322
+ const localDir = localDirRaw ? path.resolve(process.cwd(), localDirRaw) : null;
300
323
 
301
- // localDir 默认使用项目 buildDir(打包输出目录)
302
- // 规则:优先 deploy.localDir;否则使用 .env.production 的 VUE_APP_PUBLIC_URL;否则使用 .ylyxrc.json 的 buildDir;最后才回退 ./EXTERNAL_DIGIC
303
- const inferredLocalDir =
304
- deploy.localDir ||
305
- inferredFromEnvProd ||
306
- (projectConfig && projectConfig.buildDir ? path.resolve(process.cwd(), projectConfig.buildDir) : null);
307
-
308
- if (!inferredLocalDir) {
324
+ if (!localDir) {
309
325
  throw new Error(
310
- '无法确定 deploy.localDir(本地打包目录)。请设置以下任意一种:\n' +
311
- '- 在 .ylyxrc.json 配置 deploy.localDir\n' +
312
- '- 在项目 .env.production 配置 VUE_APP_PUBLIC_URL(相对路径,如 /dist/app/)\n' +
313
- '- 在 .ylyxrc.json 配置 buildDir(可相对/绝对路径)'
326
+ `无法确定环境 "${env.name}" 的本地目录(用于上传/压缩)。请在 .ylyxrc.json 该环境条目配置 localDir 或 buildDir`
314
327
  );
315
328
  }
316
329
 
317
330
  const cfg = {
318
- env: overrides.env ?? deploy.env,
319
- buildDir: overrides.buildDir ?? deploy.buildDir ?? projectConfig.buildDir,
320
- host: overrides.host ?? deploy.host,
321
- port: String(overrides.port ?? deploy.port ?? '22'),
322
- username: overrides.username ?? deploy.username,
323
- password: overrides.password ?? deploy.password,
324
- localDir: overrides.localDir ?? inferredLocalDir,
325
- remoteDir: overrides.remoteDir ?? deploy.remoteDir,
326
- zipAfter: overrides.zipAfter ?? deploy.zipAfter ?? false,
327
- zipOutDir: overrides.zipOutDir ?? deploy.zipOutDir ?? path.join(process.cwd(), '.ylyx-deploy'),
331
+ name: env.name,
332
+ deployMethod: env.deployMethod,
333
+ buildDir,
334
+ host: overrides.host ?? env.host,
335
+ port: String(overrides.port ?? env.port ?? '22'),
336
+ username: overrides.username ?? env.username,
337
+ password: overrides.password ?? env.password,
338
+ localDir,
339
+ remoteDir: normalizeAndValidateRemoteDir(overrides.remoteDir ?? env.remoteDir),
340
+ zipAfter: overrides.zipAfter ?? env.zipAfter ?? false,
341
+ zipOutDir: path.resolve(process.cwd(), overrides.zipOutDir ?? env.zipOutDir ?? './.ylyx-deploy'),
328
342
  };
329
343
 
330
344
  return cfg;
331
345
  }
332
346
 
333
347
  /**
334
- * 执行部署(递归上传目录到远端)
335
- * 测试服:上传到服务器(可选部署后压缩)
336
- * 正式服:不上传,仅压缩
337
- * @param {{env?:'test'|'prod',host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string}} overrides
348
+ * 执行部署(按环境的 deployMethod 分支:upload=SFTP 上传,zip=仅压缩)
349
+ * @param {{env?:string,host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string,buildDir?:string}} overrides
338
350
  */
339
351
  async function deploy(overrides = {}) {
340
- const cfg = resolveDeployConfig(overrides);
352
+ const projectConfig = readProjectConfig();
353
+ assertConfigMigrated(projectConfig, fs.existsSync(getConfigPath()));
354
+
355
+ const envName = await chooseDeployEnv(projectConfig, overrides.env);
356
+ const cfg = resolveDeployConfig(envName, overrides, projectConfig);
341
357
  const localFolderPath = cfg.localDir;
342
358
  const remoteFolderPath = cfg.remoteDir;
343
359
 
344
- const env = await chooseDeployEnv(cfg.env);
345
360
  const buildDirNameForLog = getBuildDirBaseName({ buildDir: cfg.buildDir, localDir: localFolderPath });
346
- const logger = createDeployLogger({ env, buildDirName: buildDirNameForLog, outDir: cfg.zipOutDir });
347
- logger.logOp('DEPLOY_START', '', `env=${env}`);
361
+ const logger = createDeployLogger({ env: cfg.name, buildDirName: buildDirNameForLog, outDir: cfg.zipOutDir });
362
+ logger.logOp('DEPLOY_START', '', `env=${cfg.name}`);
348
363
  logger.logOp('LOCAL_DIR', localFolderPath);
349
364
  if (remoteFolderPath) logger.logOp('REMOTE_DIR', remoteFolderPath);
350
365
 
@@ -354,12 +369,11 @@ async function deploy(overrides = {}) {
354
369
  throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
355
370
  }
356
371
 
357
- // 正式服:仅压缩
358
- if (env === 'prod') {
372
+ // deployMethod=zip:仅压缩,不上传
373
+ if (cfg.deployMethod === 'zip') {
359
374
  const buildDirName = buildDirNameForLog;
360
375
  const ts = formatTimestampCompact(new Date());
361
376
  const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
362
- // 优化命名:更可读、更易分割(buildDir-时间-哈希)
363
377
  const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
364
378
  fs.ensureDirSync(cfg.zipOutDir);
365
379
  const zipName = `${zipBaseName}.zip`;
@@ -377,19 +391,19 @@ async function deploy(overrides = {}) {
377
391
  await logger.close();
378
392
  throw e;
379
393
  }
380
- logger.logOp('DEPLOY_END', '', 'prod');
394
+ logger.logOp('DEPLOY_END', '', cfg.name);
381
395
  await logger.close();
382
396
  return;
383
397
  }
384
398
 
385
- // 测试服:上传到服务器
386
- if (!cfg.host) throw new Error('缺少 deploy.host(测试服上传需要)');
387
- if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要)');
388
- if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要)');
399
+ // deployMethod=upload:上传到服务器
400
+ if (!cfg.host) throw new Error(`缺少环境 "${cfg.name}" 的 host(上传需要)`);
401
+ if (!cfg.username) throw new Error(`缺少环境 "${cfg.name}" 的 username(上传需要)`);
402
+ if (!cfg.password) throw new Error(`缺少环境 "${cfg.name}" 的 password(上传需要)`);
389
403
  if (!remoteFolderPath) {
390
404
  logger.logOp('ERROR', '', 'missing_remoteDir');
391
405
  await logger.close();
392
- throw new Error('缺少 deploy.remoteDir(测试服上传需要):请在 .ylyxrc.json deploy.remoteDir 配置远端部署目录');
406
+ throw new Error(`缺少环境 "${cfg.name}" 的 remoteDir(上传需要):请在 .ylyxrc.json 该环境条目配置 remoteDir`);
393
407
  }
394
408
 
395
409
  let Client;
@@ -453,7 +467,7 @@ async function deploy(overrides = {}) {
453
467
  if (spinner) spinner.info('部署结束');
454
468
  }
455
469
 
456
- // 测试服可选:部署后压缩(延续之前的可选开关)
470
+ // 上传后可选:再压缩一份(延续之前的可选开关)
457
471
  if (cfg.zipAfter) {
458
472
  const buildDirName = buildDirNameForLog;
459
473
  const ts = formatTimestampCompact(new Date());
@@ -477,7 +491,7 @@ async function deploy(overrides = {}) {
477
491
  }
478
492
  }
479
493
 
480
- logger.logOp('DEPLOY_END', '', 'test');
494
+ logger.logOp('DEPLOY_END', '', cfg.name);
481
495
  await logger.close();
482
496
  }
483
497
 
@@ -0,0 +1,111 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+
4
+ const CONFIG_VERSION = 2;
5
+
6
+ function getConfigPath() {
7
+ return path.join(process.cwd(), '.ylyxrc.json');
8
+ }
9
+
10
+ function readConfigSafe(configPath) {
11
+ if (!fs.existsSync(configPath)) return {};
12
+ try {
13
+ return fs.readJsonSync(configPath);
14
+ } catch (e) {
15
+ throw new Error(`读取 .ylyxrc.json 失败,请确认它是有效 JSON:${e.message}`);
16
+ }
17
+ }
18
+
19
+ function isConfigMigrated(config) {
20
+ return !!config && typeof config.configVersion === 'number' && config.configVersion >= CONFIG_VERSION;
21
+ }
22
+
23
+ function assertConfigMigrated(config, configExists) {
24
+ if (!isConfigMigrated(config)) {
25
+ if (configExists === false) {
26
+ throw new Error(
27
+ '未找到 .ylyxrc.json 配置文件\n' +
28
+ ' 请先运行以下命令初始化配置:\n' +
29
+ ' ylyx init config'
30
+ );
31
+ }
32
+ throw new Error(
33
+ '检测到旧版配置(.ylyxrc.json 缺少 configVersion)\n' +
34
+ ' 请先运行以下命令完成迁移:\n' +
35
+ ' ylyx migrate config'
36
+ );
37
+ }
38
+ }
39
+
40
+ function findEnvironment(config, name) {
41
+ const environments = (config && config.environments) || [];
42
+ const env = environments.find((e) => e.name === name);
43
+ if (!env) {
44
+ const available = environments.map((e) => e.name).join(', ') || '(无)';
45
+ throw new Error(`环境不存在: ${name}\n可用环境: ${available}`);
46
+ }
47
+ return env;
48
+ }
49
+
50
+ /**
51
+ * 将旧版 .ylyxrc.json(mode/deploy/顶层 buildDir/preScripts)转换为新版 environments 结构
52
+ * 纯函数:不读写文件,方便单独验证
53
+ */
54
+ function buildMigratedConfig(oldConfig) {
55
+ oldConfig = oldConfig || {};
56
+ const oldMode = oldConfig.mode === 'prod' ? 'prod' : 'dev';
57
+ const oldDeploy = oldConfig.deploy || {};
58
+ const oldPreScripts = oldConfig.preScripts || {};
59
+ const oldBuildDir = oldConfig.buildDir;
60
+
61
+ const devEnv = {
62
+ name: 'dev',
63
+ isLocal: true,
64
+ npmScript: oldPreScripts.dev || 'dev',
65
+ deployMethod: 'none',
66
+ };
67
+
68
+ const testEnv = {
69
+ name: 'test',
70
+ npmScript: 'build:test',
71
+ deployMethod: 'upload',
72
+ buildDir: oldDeploy.buildDir || oldBuildDir || '',
73
+ host: oldDeploy.host || '',
74
+ port: oldDeploy.port || '22',
75
+ username: oldDeploy.username || '',
76
+ password: oldDeploy.password || '',
77
+ localDir: oldDeploy.localDir || '',
78
+ remoteDir: oldDeploy.remoteDir || '',
79
+ zipAfter: oldDeploy.zipAfter || false,
80
+ };
81
+
82
+ const prodEnv = {
83
+ name: 'prod',
84
+ npmScript: oldPreScripts.prod || 'build:prod',
85
+ deployMethod: 'zip',
86
+ buildDir: oldBuildDir || '',
87
+ zipOutDir: oldDeploy.zipOutDir || './.ylyx-deploy',
88
+ };
89
+
90
+ const next = { ...oldConfig };
91
+ delete next.mode;
92
+ delete next.deploy;
93
+ delete next.buildDir;
94
+ delete next.preScripts;
95
+
96
+ next.configVersion = CONFIG_VERSION;
97
+ next.currentEnv = oldMode;
98
+ next.environments = [devEnv, testEnv, prodEnv];
99
+
100
+ return next;
101
+ }
102
+
103
+ module.exports = {
104
+ CONFIG_VERSION,
105
+ getConfigPath,
106
+ readConfigSafe,
107
+ isConfigMigrated,
108
+ assertConfigMigrated,
109
+ findEnvironment,
110
+ buildMigratedConfig,
111
+ };
package/package.json CHANGED
@@ -1,59 +1,61 @@
1
- {
2
- "name": "ylyx-cli",
3
- "version": "1.0.15",
4
- "description": "公司内部代码生成模板脚手架工具,支持快速生成项目初始结构和代码模板",
5
- "main": "lib/index.js",
6
- "bin": {
7
- "ylyx": "./bin/ylyx.js",
8
- "ylyx-cli": "./bin/ylyx.js"
9
- },
10
- "files": [
11
- "bin",
12
- "lib",
13
- "templates",
14
- "README.md"
15
- ],
16
- "scripts": {
17
- "start": "node bin/ylyx.js",
18
- "test": "echo \"Error: no test specified\" && exit 1"
19
- },
20
- "keywords": [
21
- "cli",
22
- "scaffold",
23
- "code-generator",
24
- "template",
25
- "generator",
26
- "scaffolding",
27
- "boilerplate",
28
- "project-template",
29
- "vue",
30
- "react"
31
- ],
32
- "author": "YLYX",
33
- "license": "ISC",
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/your-org/ylyx-cli.git"
37
- },
38
- "bugs": {
39
- "url": "https://github.com/your-org/ylyx-cli/issues"
40
- },
41
- "homepage": "https://github.com/your-org/ylyx-cli#readme",
42
- "engines": {
43
- "node": ">=12.0.0"
44
- },
45
- "dependencies": {
46
- "archiver": "^5.3.2",
47
- "commander": "^11.1.0",
48
- "inquirer": "^8.2.6",
49
- "chalk": "^4.1.2",
50
- "fs-extra": "^11.2.0",
51
- "handlebars": "^4.7.8",
52
- "glob": "^10.3.10",
53
- "ora": "^5.4.1",
54
- "ssh2-sftp-client": "^9.0.4"
55
- },
56
- "devDependencies": {
57
- "@types/node": "^20.10.0"
58
- }
59
- }
1
+ {
2
+ "name": "ylyx-cli",
3
+ "version": "1.1.0",
4
+ "description": "公司内部代码生成模板脚手架工具,支持快速生成项目初始结构和代码模板",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "ylyx": "./bin/ylyx.js",
8
+ "ylyx-cli": "./bin/ylyx.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "lib/**/*.js",
13
+ "!lib/**/*.test.js",
14
+ "templates",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "start": "node bin/ylyx.js",
19
+ "test": "jest"
20
+ },
21
+ "keywords": [
22
+ "cli",
23
+ "scaffold",
24
+ "code-generator",
25
+ "template",
26
+ "generator",
27
+ "scaffolding",
28
+ "boilerplate",
29
+ "project-template",
30
+ "vue",
31
+ "react"
32
+ ],
33
+ "author": "YLYX",
34
+ "license": "ISC",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/your-org/ylyx-cli.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/your-org/ylyx-cli/issues"
41
+ },
42
+ "homepage": "https://github.com/your-org/ylyx-cli#readme",
43
+ "engines": {
44
+ "node": ">=12.0.0"
45
+ },
46
+ "dependencies": {
47
+ "archiver": "^5.3.2",
48
+ "commander": "^11.1.0",
49
+ "inquirer": "^8.2.6",
50
+ "chalk": "^4.1.2",
51
+ "fs-extra": "^11.2.0",
52
+ "handlebars": "^4.7.8",
53
+ "glob": "^10.3.10",
54
+ "ora": "^5.4.1",
55
+ "ssh2-sftp-client": "^9.0.4"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^20.10.0",
59
+ "jest": "^29.7.0"
60
+ }
61
+ }
@@ -0,0 +1,22 @@
1
+ # dependencies
2
+ /node_modules
3
+ /.pnp
4
+ .pnp.js
5
+
6
+ # testing
7
+ /coverage
8
+
9
+ # production
10
+ /build
11
+
12
+ # misc
13
+ .DS_Store
14
+ .env.local
15
+ .env.development.local
16
+ .env.test.local
17
+ .env.production.local
18
+
19
+ npm-debug.log*
20
+ yarn-debug.log*
21
+ yarn-error.log*
22
+
@@ -0,0 +1,24 @@
1
+ .DS_Store
2
+ node_modules/
3
+ dist/
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ **/*.log
8
+ hospManage/
9
+ EXTERNAL_HOSPITAL/
10
+ tests/**/coverage/
11
+ tests/e2e/reports
12
+ selenium-debug.log
13
+
14
+ # Editor directories and files
15
+ .idea
16
+ # .vscode
17
+ *.suo
18
+ *.ntvs*
19
+ *.njsproj
20
+ *.sln
21
+ *.local
22
+
23
+ package-lock.json
24
+ yarn.lock