@snack-kit/scripts 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -11
- package/bin/lib.js +91 -0
- package/bin/snack-cli.js +280 -0
- package/bin/snack-scripts.js +162 -0
- package/package.json +5 -5
- package/utils/package.js +15 -0
- package/bin/main.js +0 -478
package/README.md
CHANGED
|
@@ -10,15 +10,25 @@ npm install @snack-kit/scripts --save-dev
|
|
|
10
10
|
|
|
11
11
|
## 命令
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|------------------------|---------------|
|
|
15
|
-
| `snack-cli init [dir]` | 初始化新 snack 工程 |
|
|
16
|
-
| `snack-cli create` | 在当前工程创建新模块模版 |
|
|
17
|
-
| `snack-cli start` | 启动开发调试服务 |
|
|
18
|
-
| `snack-cli build` | 生产模式打包 |
|
|
19
|
-
| `snack-cli entry` | 打包独立入口页面 |
|
|
13
|
+
### snack-cli — 全局脚手架
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
全局安装后使用,用于初始化工程和创建模块。
|
|
16
|
+
|
|
17
|
+
| 命令 | 说明 |
|
|
18
|
+
|------------------------|-----------------|
|
|
19
|
+
| `snack-cli init [dir]` | 初始化新 snack 工程 |
|
|
20
|
+
| `snack-cli create` | 在当前工程创建新模块模版 |
|
|
21
|
+
| `snack-cli -v` | 查看当前版本号 |
|
|
22
|
+
|
|
23
|
+
### snack-scripts — 项目构建
|
|
24
|
+
|
|
25
|
+
安装到项目 `devDependencies` 后通过 `npm run` 调用,用于开发调试和生产打包。
|
|
26
|
+
|
|
27
|
+
| 命令 | 说明 |
|
|
28
|
+
|-----------------------|------------|
|
|
29
|
+
| `snack-scripts start` | 启动开发调试服务 |
|
|
30
|
+
| `snack-scripts build` | 生产模式打包 |
|
|
31
|
+
| `snack-scripts entry` | 打包独立入口页面 |
|
|
22
32
|
|
|
23
33
|
### init — 初始化工程
|
|
24
34
|
|
|
@@ -75,9 +85,12 @@ src/package/UserManager/
|
|
|
75
85
|
```json
|
|
76
86
|
{
|
|
77
87
|
"scripts": {
|
|
78
|
-
"start": "snack-
|
|
79
|
-
"build": "snack-
|
|
80
|
-
"entry": "snack-
|
|
88
|
+
"start": "snack-scripts start",
|
|
89
|
+
"build": "snack-scripts build",
|
|
90
|
+
"entry": "snack-scripts entry"
|
|
91
|
+
},
|
|
92
|
+
"devDependencies": {
|
|
93
|
+
"@snack-kit/scripts": "^0.4.0"
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
```
|
|
@@ -170,6 +183,14 @@ module.exports = (config) => {
|
|
|
170
183
|
|
|
171
184
|
## Changelog
|
|
172
185
|
|
|
186
|
+
### 0.8.0
|
|
187
|
+
|
|
188
|
+
- 重构:`bin/main.js` 拆分为 `snack-cli`(init / create)和 `snack-scripts`(start / build / entry)两个独立命令
|
|
189
|
+
- 新增:`snack-cli -v` 查看当前版本号
|
|
190
|
+
- 新增:共享工具模块 `bin/lib.js`(语言检测、i18n 字符串、交互式提问工具)
|
|
191
|
+
- 修复:`init` 生成项目的 `devDependencies` 新增 `@snack-kit/scripts`,本地安装后不再依赖全局版本
|
|
192
|
+
- 修复:`init` 生成的 `scripts` 命令修正为 `snack-scripts`,与实际 bin 名一致
|
|
193
|
+
|
|
173
194
|
### 0.7.0
|
|
174
195
|
|
|
175
196
|
- 修复: 修复 loader.js 打包丢失的问题
|
package/bin/lib.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
// ─── 语言检测 ─────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function detectLang() {
|
|
8
|
+
const env = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || process.env.LC_MESSAGES || '';
|
|
9
|
+
return env.toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const lang = detectLang();
|
|
13
|
+
|
|
14
|
+
// ─── i18n 字符串表 ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const t = {
|
|
17
|
+
// init
|
|
18
|
+
initTitle: { zh: ' Snack 项目初始化', en: ' Snack Project Init' },
|
|
19
|
+
projectName: { zh: '项目名称', en: 'Project name' },
|
|
20
|
+
description: { zh: '项目描述', en: 'Description' },
|
|
21
|
+
author: { zh: '作者', en: 'Author' },
|
|
22
|
+
debugServers: { zh: '调试服务器(多个用逗号分隔)', en: 'Debug servers (comma separated)' },
|
|
23
|
+
entryPageId: { zh: '入口页面 ID', en: 'Entry page id' },
|
|
24
|
+
entryPageType: { zh: '入口页面类型', en: 'Entry page type' },
|
|
25
|
+
reactVersion: { zh: 'React 版本', en: 'React version' },
|
|
26
|
+
projectCreated: { zh: ' 项目已创建:', en: ' Project created at:' },
|
|
27
|
+
nextSteps: { zh: ' 后续步骤:', en: ' Next steps:' },
|
|
28
|
+
// create
|
|
29
|
+
createTitle: { zh: ' Snack 模块创建', en: ' Snack Module Create' },
|
|
30
|
+
moduleName: { zh: '模块名称', en: 'Module name' },
|
|
31
|
+
moduleDirName: { zh: '模块目录名(PascalCase)', en: 'Module directory name (PascalCase)' },
|
|
32
|
+
withSetting: { zh: '生成配置模块?(y/n)', en: 'Generate setting module? (y/n)' },
|
|
33
|
+
moduleCreated: { zh: ' 模块已创建:', en: ' Module created:' },
|
|
34
|
+
filesGenerated: { zh: ' 已生成文件:', en: ' Files generated:' },
|
|
35
|
+
// errors
|
|
36
|
+
errNoPkg: { zh: '错误:当前目录没有 package.json,请在 snack 工程根目录下执行该命令', en: 'Error: No package.json found. Run this command in a snack project root.' },
|
|
37
|
+
errDirRequired: { zh: '错误:模块目录名不能为空', en: 'Error: Module directory name is required' },
|
|
38
|
+
errDirExists: { zh: '错误:模块目录已存在:', en: 'Error: Module directory already exists:' },
|
|
39
|
+
errNameConflict: { zh: '请修改 snack-cli package.json "name" 的默认名称,该名称将在部署时作为分类目录', en: 'Please change the default "name" in package.json. It will be used as a category directory during deployment.' },
|
|
40
|
+
errNoEntry: { zh: 'package.json 未找到 snack.entry 配置项', en: 'snack.entry config not found in package.json' },
|
|
41
|
+
errTempDir: { zh: '临时目录创建失败', en: 'Failed to create temp directory' },
|
|
42
|
+
errIntegrity: { zh: '完整性校验失败', en: 'Integrity check failed' },
|
|
43
|
+
// build logs
|
|
44
|
+
clearOutputDir: { zh: '清理输出目录', en: 'clear output dir' },
|
|
45
|
+
clearOutputDirEnd: { zh: '清理完成', en: 'clear output dir end' },
|
|
46
|
+
devServerRunning: { zh: 'Snack 开发服务器运行中...', en: 'Snack DevServer Runing...' },
|
|
47
|
+
createLoaderAndHTML: { zh: '生成 loader.js 和入口 HTML(并行)...', en: 'create loader.js & entry HTML (parallel) ...' },
|
|
48
|
+
createLoaderAndHTMLEnd: { zh: '生成完成', en: 'create loader.js & entry HTML end' },
|
|
49
|
+
createSnack: { zh: '生成 snack 模块...', en: 'create snack ...' },
|
|
50
|
+
createSnackEnd: { zh: '生成完成', en: 'create snack end' },
|
|
51
|
+
createEntry: { zh: '生成入口页...', en: 'create entry ...' },
|
|
52
|
+
integrityPass: { zh: '完整性校验通过', en: 'Integrity check passed' },
|
|
53
|
+
integrityFileMissing: { zh: '完整性校验:文件缺失', en: 'snack check: file is missing' },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function i(key) {
|
|
57
|
+
return t[key][lang];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── 交互式提问工具 ───────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function prompt(rl, question, defaultValue) {
|
|
63
|
+
return new Promise(resolve => {
|
|
64
|
+
const hint = defaultValue !== undefined ? ` (${defaultValue})` : '';
|
|
65
|
+
rl.question(`${question}${hint}: `, answer => {
|
|
66
|
+
const val = answer.trim();
|
|
67
|
+
resolve(val === '' && defaultValue !== undefined ? defaultValue : val);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function select(rl, question, options, defaultValue) {
|
|
73
|
+
return new Promise(resolve => {
|
|
74
|
+
const hint = options.map((o, idx) => `${idx + 1}) ${o}`).join(' ');
|
|
75
|
+
const defaultHint = options.includes(defaultValue) ? defaultValue : options[0];
|
|
76
|
+
rl.question(`${question} [${hint}] (${defaultHint}): `, answer => {
|
|
77
|
+
const val = answer.trim();
|
|
78
|
+
if (val === '') return resolve(defaultHint);
|
|
79
|
+
const num = parseInt(val, 10);
|
|
80
|
+
if (!isNaN(num) && num >= 1 && num <= options.length) return resolve(options[num - 1]);
|
|
81
|
+
if (options.includes(val)) return resolve(val);
|
|
82
|
+
resolve(defaultHint);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createRl() {
|
|
88
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { lang, i, prompt, select, createRl };
|
package/bin/snack-cli.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* snack-cli — 全局脚手架命令
|
|
6
|
+
*
|
|
7
|
+
* 命令:
|
|
8
|
+
* snack-cli init [dir] 初始化新 snack 工程
|
|
9
|
+
* snack-cli create 在当前工程创建新模块模版
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const minimist = require('minimist');
|
|
14
|
+
const fs = require('fs-extra');
|
|
15
|
+
const { version } = require('../package.json');
|
|
16
|
+
const { lang, i, prompt, select, createRl } = require('./lib');
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const argv = minimist(args);
|
|
20
|
+
const script = args[0];
|
|
21
|
+
|
|
22
|
+
// ─── init 命令:初始化新 snack 工程 ──────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
async function runInit() {
|
|
25
|
+
const targetDir = argv._[1] ? path.resolve(process.cwd(), argv._[1]) : process.cwd();
|
|
26
|
+
const dirName = path.basename(targetDir);
|
|
27
|
+
|
|
28
|
+
console.log(`\n${i('initTitle')}\n`);
|
|
29
|
+
|
|
30
|
+
const rl = createRl();
|
|
31
|
+
|
|
32
|
+
const name = await prompt(rl, i('projectName'), dirName);
|
|
33
|
+
const description = await prompt(rl, i('description'), '');
|
|
34
|
+
const author = await prompt(rl, i('author'), '');
|
|
35
|
+
const debugInput = await prompt(rl, i('debugServers'), 'http://127.0.0.1:3000');
|
|
36
|
+
const entryId = await prompt(rl, i('entryPageId'), name);
|
|
37
|
+
const entryType = await prompt(rl, i('entryPageType'), 'portal');
|
|
38
|
+
const reactVersion = await select(rl, i('reactVersion'), ['17', '18', '19'], '19');
|
|
39
|
+
|
|
40
|
+
rl.close();
|
|
41
|
+
|
|
42
|
+
const debugServers = debugInput.split(',').map(s => s.trim()).filter(Boolean);
|
|
43
|
+
|
|
44
|
+
const reactVersionMap = {
|
|
45
|
+
'17': { react: '^17.0.0', reactDom: '^17.0.0', typesReact: '^17.0.0', typesReactDom: '^17.0.0' },
|
|
46
|
+
'18': { react: '^18.0.0', reactDom: '^18.0.0', typesReact: '^18.0.0', typesReactDom: '^18.0.0' },
|
|
47
|
+
'19': { react: '^19.0.0', reactDom: '^19.0.0', typesReact: '^19.0.0', typesReactDom: '^19.0.0' },
|
|
48
|
+
};
|
|
49
|
+
const reactDeps = reactVersionMap[reactVersion];
|
|
50
|
+
|
|
51
|
+
// ── package.json ─────────────────────────────────────────────────────────
|
|
52
|
+
const pkg = {
|
|
53
|
+
name,
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
description,
|
|
56
|
+
scripts: {
|
|
57
|
+
start: 'snack-scripts start',
|
|
58
|
+
build: 'snack-scripts build',
|
|
59
|
+
entry: 'snack-scripts entry'
|
|
60
|
+
},
|
|
61
|
+
input: './src/package',
|
|
62
|
+
output: './dist',
|
|
63
|
+
snack: {
|
|
64
|
+
externals: {},
|
|
65
|
+
buildIgnore: [],
|
|
66
|
+
devPackage: [],
|
|
67
|
+
entry: {
|
|
68
|
+
name: 'entry',
|
|
69
|
+
id: entryId,
|
|
70
|
+
type: entryType,
|
|
71
|
+
title: '',
|
|
72
|
+
favicon: '',
|
|
73
|
+
mobile: { id: `${entryId}_mobile`, type: entryType }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
author,
|
|
77
|
+
license: 'ISC',
|
|
78
|
+
dependencies: {
|
|
79
|
+
'@snack-kit/core': '^0.3.0',
|
|
80
|
+
'react': reactDeps.react,
|
|
81
|
+
'react-dom': reactDeps.reactDom
|
|
82
|
+
},
|
|
83
|
+
devDependencies: {
|
|
84
|
+
'@snack-kit/scripts': `^${version}`,
|
|
85
|
+
'@types/react': reactDeps.typesReact,
|
|
86
|
+
'@types/react-dom': reactDeps.typesReactDom
|
|
87
|
+
},
|
|
88
|
+
dev: { debug: debugServers }
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ── tsconfig.json ─────────────────────────────────────────────────────────
|
|
92
|
+
const tsconfig = {
|
|
93
|
+
compilerOptions: {
|
|
94
|
+
target: 'es5',
|
|
95
|
+
module: 'es6',
|
|
96
|
+
outDir: './dist/',
|
|
97
|
+
baseUrl: '.',
|
|
98
|
+
experimentalDecorators: true,
|
|
99
|
+
allowJs: true,
|
|
100
|
+
skipLibCheck: true,
|
|
101
|
+
esModuleInterop: true,
|
|
102
|
+
allowSyntheticDefaultImports: true,
|
|
103
|
+
strict: true,
|
|
104
|
+
forceConsistentCasingInFileNames: true,
|
|
105
|
+
moduleResolution: 'node',
|
|
106
|
+
resolveJsonModule: true,
|
|
107
|
+
isolatedModules: true,
|
|
108
|
+
jsx: 'react-jsx'
|
|
109
|
+
},
|
|
110
|
+
include: ['src'],
|
|
111
|
+
exclude: ['node_modules', 'dist']
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
115
|
+
const gitignore = `node_modules
|
|
116
|
+
dist
|
|
117
|
+
.snack
|
|
118
|
+
*.local
|
|
119
|
+
.DS_Store
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
// ── src/env.d.ts ──────────────────────────────────────────────────────────
|
|
123
|
+
const envDts = `declare module '*.svg'
|
|
124
|
+
declare module '*.png'
|
|
125
|
+
declare module '*.jpg'
|
|
126
|
+
declare module '*.jpeg'
|
|
127
|
+
declare module '*.gif'
|
|
128
|
+
declare module '*.bmp'
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
// ── 写入文件 ───────────────────────────────────────────────────────────────
|
|
132
|
+
await fs.ensureDir(targetDir);
|
|
133
|
+
await fs.outputJSON(path.join(targetDir, 'package.json'), pkg, { spaces: 4 });
|
|
134
|
+
await fs.outputJSON(path.join(targetDir, 'tsconfig.json'), tsconfig, { spaces: 4 });
|
|
135
|
+
await fs.outputFile(path.join(targetDir, '.gitignore'), gitignore);
|
|
136
|
+
await fs.ensureDir(path.join(targetDir, 'src/package'));
|
|
137
|
+
await fs.outputFile(path.join(targetDir, 'src/env.d.ts'), envDts);
|
|
138
|
+
|
|
139
|
+
console.log(`
|
|
140
|
+
${i('projectCreated')} ${targetDir}
|
|
141
|
+
|
|
142
|
+
${i('nextSteps')}
|
|
143
|
+
${targetDir !== process.cwd() ? `cd ${argv._[1]}\n ` : ''}npm install
|
|
144
|
+
npm run start
|
|
145
|
+
`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── create 命令:在当前工程创建新模块模版 ────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
async function runCreate() {
|
|
151
|
+
const projectPath = argv.project || process.cwd();
|
|
152
|
+
const pkgPath = path.resolve(projectPath, 'package.json');
|
|
153
|
+
if (!fs.existsSync(pkgPath)) {
|
|
154
|
+
console.error(i('errNoPkg'));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const projectPkg = fs.readJSONSync(pkgPath);
|
|
158
|
+
const packageDir = path.resolve(projectPath, projectPkg.input || 'src/package');
|
|
159
|
+
|
|
160
|
+
console.log(`\n${i('createTitle')}\n`);
|
|
161
|
+
|
|
162
|
+
const rl = createRl();
|
|
163
|
+
|
|
164
|
+
const moduleName = await prompt(rl, i('moduleName'), lang === 'zh' ? '新模块' : 'New Module');
|
|
165
|
+
let dirName = await prompt(rl, i('moduleDirName'), '');
|
|
166
|
+
const description = await prompt(rl, i('description'), '');
|
|
167
|
+
const author = await prompt(rl, i('author'), projectPkg.author || '');
|
|
168
|
+
const withSetting = (await prompt(rl, i('withSetting'), 'y')).toLowerCase() === 'y';
|
|
169
|
+
|
|
170
|
+
rl.close();
|
|
171
|
+
|
|
172
|
+
if (!dirName) {
|
|
173
|
+
console.error(i('errDirRequired'));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
dirName = dirName.charAt(0).toUpperCase() + dirName.slice(1);
|
|
178
|
+
const moduleDir = path.join(packageDir, dirName);
|
|
179
|
+
const className = dirName;
|
|
180
|
+
|
|
181
|
+
if (fs.existsSync(moduleDir)) {
|
|
182
|
+
console.error(`${i('errDirExists')} ${moduleDir}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── snack.json ────────────────────────────────────────────────────────────
|
|
187
|
+
const snackJson = {
|
|
188
|
+
name: moduleName,
|
|
189
|
+
version: '0.0.0.1',
|
|
190
|
+
author,
|
|
191
|
+
description
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// ── index.ts ──────────────────────────────────────────────────────────────
|
|
195
|
+
const indexTs = `export * from './src/index';
|
|
196
|
+
${withSetting ? "export * from './src/setting';\n" : ''}`;
|
|
197
|
+
|
|
198
|
+
// ── src/index.tsx ─────────────────────────────────────────────────────────
|
|
199
|
+
const indexTsx = `import React from 'react';
|
|
200
|
+
import { Snack, SnackData } from '@snack-kit/core';
|
|
201
|
+
import './style/index.scss';
|
|
202
|
+
|
|
203
|
+
interface Props extends SnackData {}
|
|
204
|
+
|
|
205
|
+
export class ${className} extends Snack {
|
|
206
|
+
constructor(data: Props = {}) {
|
|
207
|
+
super(data);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public $component(props: Props): React.ReactNode {
|
|
211
|
+
return (
|
|
212
|
+
<div className={'${className.toLowerCase()}'}>
|
|
213
|
+
{/* TODO */}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default ${className};
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
// ── src/setting.tsx ───────────────────────────────────────────────────────
|
|
223
|
+
const settingTsx = `import React from 'react';
|
|
224
|
+
import { SnackSetting } from '@snack-kit/core';
|
|
225
|
+
import Main from './index';
|
|
226
|
+
import './style/setting.scss';
|
|
227
|
+
|
|
228
|
+
interface Props {}
|
|
229
|
+
|
|
230
|
+
export class ${className}Setting extends SnackSetting {
|
|
231
|
+
constructor(public main: Main, public data: Props) {
|
|
232
|
+
super(data);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public $component(): React.ReactNode {
|
|
236
|
+
return <></>;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
// ── 样式文件 ───────────────────────────────────────────────────────────────
|
|
242
|
+
const indexScss = `.${className.toLowerCase()} {\n}\n`;
|
|
243
|
+
const settingScss = `.${className.toLowerCase()}-setting {\n}\n`;
|
|
244
|
+
|
|
245
|
+
// ── 写入文件 ───────────────────────────────────────────────────────────────
|
|
246
|
+
await fs.outputJSON(path.join(moduleDir, 'snack.json'), snackJson, { spaces: 4 });
|
|
247
|
+
await fs.outputFile(path.join(moduleDir, 'index.ts'), indexTs);
|
|
248
|
+
await fs.outputFile(path.join(moduleDir, 'src/index.tsx'), indexTsx);
|
|
249
|
+
await fs.outputFile(path.join(moduleDir, 'src/style/index.scss'), indexScss);
|
|
250
|
+
|
|
251
|
+
if (withSetting) {
|
|
252
|
+
await fs.outputFile(path.join(moduleDir, 'src/setting.tsx'), settingTsx);
|
|
253
|
+
await fs.outputFile(path.join(moduleDir, 'src/style/setting.scss'), settingScss);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log(`
|
|
257
|
+
${i('moduleCreated')} ${moduleDir}
|
|
258
|
+
|
|
259
|
+
${i('filesGenerated')}
|
|
260
|
+
${dirName}/snack.json
|
|
261
|
+
${dirName}/index.ts
|
|
262
|
+
${dirName}/src/index.tsx
|
|
263
|
+
${dirName}/src/style/index.scss${withSetting ? `
|
|
264
|
+
${dirName}/src/setting.tsx
|
|
265
|
+
${dirName}/src/style/setting.scss` : ''}
|
|
266
|
+
`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── 入口路由 ─────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
if (script === 'init') {
|
|
272
|
+
runInit().catch(err => { console.error(err); process.exit(1); });
|
|
273
|
+
} else if (script === 'create') {
|
|
274
|
+
runCreate().catch(err => { console.error(err); process.exit(1); });
|
|
275
|
+
} else if (script === '-v' || script === '--version') {
|
|
276
|
+
console.log(version);
|
|
277
|
+
} else {
|
|
278
|
+
console.error(`snack-cli: unknown command "${script || ''}"\nUsage: snack-cli <init|create|-v>`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* snack-scripts — 项目构建 / 调试命令
|
|
6
|
+
*
|
|
7
|
+
* 命令:
|
|
8
|
+
* snack-scripts start 启动开发调试服务
|
|
9
|
+
* snack-scripts build 生产模式打包
|
|
10
|
+
* snack-scripts entry 打包独立入口页面
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const minimist = require('minimist');
|
|
16
|
+
const fs = require('fs-extra');
|
|
17
|
+
const { version } = require('../package.json');
|
|
18
|
+
const { GetPackageList } = require('../utils/package');
|
|
19
|
+
const { i } = require('./lib');
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const argv = minimist(args, { default: { project: null } });
|
|
23
|
+
const scriptIndex = args.findIndex(x => x === 'build' || x === 'start' || x === 'entry');
|
|
24
|
+
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
|
|
25
|
+
|
|
26
|
+
let projectPath = argv.project || process.cwd();
|
|
27
|
+
let templatePath = argv.templatePath
|
|
28
|
+
? path.resolve(projectPath, argv.templatePath)
|
|
29
|
+
: path.resolve(__dirname, '../template');
|
|
30
|
+
|
|
31
|
+
const snackPath = path.resolve(__dirname, '../config/webpack.snack.config.js');
|
|
32
|
+
const indexHTMLPath = path.resolve(__dirname, '../config/webpack.index.config.js');
|
|
33
|
+
const loaderConfigPath = path.resolve(__dirname, '../config/webpack.loader.config.js');
|
|
34
|
+
const devConfigPath = path.resolve(__dirname, '../config/webpack.dev.config.js');
|
|
35
|
+
const entryConfigPath = path.resolve(__dirname, '../config/webpack.entry.config.js');
|
|
36
|
+
|
|
37
|
+
const platform = process.platform;
|
|
38
|
+
const webpackFile = platform === 'win32' ? 'webpack.cmd' : 'webpack';
|
|
39
|
+
const isNpm = fs.existsSync(path.resolve(process.cwd(), './node_modules/.bin/', webpackFile));
|
|
40
|
+
|
|
41
|
+
// ─── webpack 构建命令 ─────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function execWebpack(wpArgs) {
|
|
44
|
+
return new Promise(resolve => {
|
|
45
|
+
const ps = isNpm
|
|
46
|
+
? spawn('webpack', wpArgs, { stdio: 'inherit', shell: true })
|
|
47
|
+
: spawn(path.resolve(__dirname, '../node_modules/.bin/', webpackFile), wpArgs, { stdio: 'inherit' });
|
|
48
|
+
ps.on('error', e => { console.log(e); resolve(false); });
|
|
49
|
+
ps.on('exit', () => resolve(true));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearOutput(output) {
|
|
54
|
+
fs.removeSync(output);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function check(output) {
|
|
58
|
+
try {
|
|
59
|
+
const list = await fs.readdir(output);
|
|
60
|
+
for (const dirname of list) {
|
|
61
|
+
if (dirname === '__common__') continue;
|
|
62
|
+
const snackjson = path.join(output, dirname, 'snack.json');
|
|
63
|
+
if (!await fs.exists(snackjson)) continue;
|
|
64
|
+
const index = path.join(output, dirname, 'index.js');
|
|
65
|
+
if (!await fs.exists(index)) {
|
|
66
|
+
console.error(`${i('integrityFileMissing')}: ${index}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('snack check: ', err);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
console.log(i('integrityPass'));
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function run() {
|
|
79
|
+
const projectPackageJSON = fs.readJSONSync(path.resolve(projectPath, 'package.json'));
|
|
80
|
+
if (projectPackageJSON.name === 'snack-cli') {
|
|
81
|
+
throw i('errNameConflict');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.env.SNACK_CLI_VERSION = version;
|
|
85
|
+
process.env.RUN_SCRIPT = script;
|
|
86
|
+
process.env.SNACK_SCRIPTS_PATH = __dirname;
|
|
87
|
+
process.env.PROJECT_COMMON_NAME = '__common__';
|
|
88
|
+
process.env.PROJECT_PACKAGE_JSON = JSON.stringify(projectPackageJSON);
|
|
89
|
+
process.env.PROJECT_TYPE = projectPackageJSON.name;
|
|
90
|
+
process.env.PROJECT_PATH = projectPath;
|
|
91
|
+
process.env.PROJECT_PACKAGE_PATH = path.resolve(projectPath, projectPackageJSON.input || 'src/package');
|
|
92
|
+
process.env.PROJECT_TEMP_PATH = path.resolve(projectPath, '.snack');
|
|
93
|
+
process.env.PROJECT_TEMPLATE_PATH = path.join(process.env.PROJECT_TEMP_PATH, 'template');
|
|
94
|
+
|
|
95
|
+
await fs.remove(process.env.PROJECT_TEMPLATE_PATH);
|
|
96
|
+
const err = await fs.copy(templatePath, process.env.PROJECT_TEMPLATE_PATH);
|
|
97
|
+
if (err) {
|
|
98
|
+
console.error(i('errTempDir'));
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
process.env.PROJECT_SNACK_CONFIG = JSON.stringify(projectPackageJSON.snack || {});
|
|
103
|
+
|
|
104
|
+
if (script === 'entry') {
|
|
105
|
+
if (!projectPackageJSON.snack?.entry) throw i('errNoEntry');
|
|
106
|
+
process.env.RUN_MODE = 'production';
|
|
107
|
+
process.env.PROJECT_OUT_PATH = path.resolve(projectPath, projectPackageJSON.snack.entry.name || 'entry');
|
|
108
|
+
console.log(i('createEntry'));
|
|
109
|
+
await execWebpack(['--mode=production', `--config=${entryConfigPath}`]);
|
|
110
|
+
} else {
|
|
111
|
+
if (script === 'start') {
|
|
112
|
+
process.env.RUN_MODE = 'development';
|
|
113
|
+
process.env.PROJECT_OUT_PATH = path.resolve(process.env.PROJECT_TEMP_PATH, 'temp');
|
|
114
|
+
} else {
|
|
115
|
+
process.env.RUN_MODE = 'production';
|
|
116
|
+
process.env.PROJECT_OUT_PATH = path.resolve(projectPath, projectPackageJSON.output || 'dist', projectPackageJSON.name);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
process.env.PROJECT_PACKAGE_LIST = JSON.stringify(GetPackageList(process.env.PROJECT_PACKAGE_PATH));
|
|
120
|
+
|
|
121
|
+
console.log(`${i('clearOutputDir')} "${process.env.PROJECT_OUT_PATH}" ...`);
|
|
122
|
+
clearOutput(process.env.PROJECT_OUT_PATH);
|
|
123
|
+
console.log(i('clearOutputDirEnd'));
|
|
124
|
+
|
|
125
|
+
if (script === 'start') {
|
|
126
|
+
console.log(i('devServerRunning'));
|
|
127
|
+
await execWebpack(['serve', '--mode=development', `--config=${devConfigPath}`]);
|
|
128
|
+
} else {
|
|
129
|
+
console.log(i('createLoaderAndHTML'));
|
|
130
|
+
await Promise.all([
|
|
131
|
+
execWebpack(['--mode=production', `--config=${loaderConfigPath}`]),
|
|
132
|
+
execWebpack(['--mode=production', `--config=${indexHTMLPath}`])
|
|
133
|
+
]);
|
|
134
|
+
console.log(i('createLoaderAndHTMLEnd'));
|
|
135
|
+
console.log(i('createSnack'));
|
|
136
|
+
await execWebpack(['--mode=production', `--config=${snackPath}`]);
|
|
137
|
+
console.log(i('createSnackEnd'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (process.env.RUN_MODE !== 'development' && !await check(process.env.PROJECT_OUT_PATH)) {
|
|
141
|
+
fs.removeSync(process.env.PROJECT_OUT_PATH);
|
|
142
|
+
throw i('errIntegrity');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`
|
|
147
|
+
_____ __ ________ ____
|
|
148
|
+
/ ___/____ ____ ______/ /__ / ____/ / / _/
|
|
149
|
+
\\__ \\/ __ \\/ __ \`/ ___/ //_/ / / / / / /
|
|
150
|
+
___/ / / / / /_/ / /__/ ,< / /___/ /____/ /
|
|
151
|
+
/____/_/ /_/\\__,_/\\___/_/|_| \\____/_____/___/ v.${version}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── 入口路由 ─────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
if (script === 'start' || script === 'build' || script === 'entry') {
|
|
158
|
+
run().catch(err => { console.error(err); process.exit(1); });
|
|
159
|
+
} else {
|
|
160
|
+
console.error(`snack-scripts: unknown command "${script || ''}"\nUsage: snack-scripts <start|build|entry>`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snack-kit/scripts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "snack-cli package scripts Powered by Para FED",
|
|
5
5
|
"bin": {
|
|
6
|
-
"snack-
|
|
7
|
-
"snack-
|
|
6
|
+
"snack-cli": "./bin/snack-cli.js",
|
|
7
|
+
"snack-scripts": "./bin/snack-scripts.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"tsconfig.json"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"dev:start": "node bin/
|
|
20
|
-
"dev:build": "node bin/
|
|
19
|
+
"dev:start": "node bin/snack-scripts.js start --project=/Users/liujia/工作/snack/code-editor",
|
|
20
|
+
"dev:build": "node bin/snack-scripts.js build --project=/Users/liujia/工作/snack3-test",
|
|
21
21
|
"release": "npm publish --access public",
|
|
22
22
|
"release:beta": "npm publish --tag=beta --access public",
|
|
23
23
|
"reinstall": "rm -rf node_modules && npm i"
|
package/utils/package.js
CHANGED
|
@@ -62,6 +62,21 @@ module.exports.GetPackageList = (projectPath) => {
|
|
|
62
62
|
const runtimeEntry = path.resolve(snackPath, 'src', 'index.tsx');
|
|
63
63
|
if (!fs.existsSync(runtimeEntry)) throw new Error(`Package(${dirName}) entry "${runtimeEntry}" is missing`);
|
|
64
64
|
|
|
65
|
+
// 检测 dirName 是否作为具名导出存在于入口文件链中
|
|
66
|
+
const exportPattern = new RegExp(
|
|
67
|
+
`export\\s+class\\s+${dirName}\\b` + // export class Foo
|
|
68
|
+
`|export\\s*\\{[^}]*\\bas\\s+${dirName}\\b[^}]*\\}` + // export { X as Foo }
|
|
69
|
+
`|export\\s*\\{[^}]*\\b${dirName}\\b[^}]*\\}` // export { Foo }
|
|
70
|
+
);
|
|
71
|
+
const filesToCheck = [entry, runtimeEntry];
|
|
72
|
+
const hasExport = filesToCheck.some(f => exportPattern.test(fs.readFileSync(f, 'utf-8')));
|
|
73
|
+
if (!hasExport) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Package(${dirName}) missing named export "${dirName}".\n` +
|
|
76
|
+
` Expected: "export class ${dirName}" or "export { ... as ${dirName} }" in index.ts or src/index.tsx`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
65
80
|
acc.push({
|
|
66
81
|
name: dirName.toLocaleLowerCase(),
|
|
67
82
|
dirName,
|
package/bin/main.js
DELETED
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const { spawn } = require('child_process');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const readline = require('readline');
|
|
7
|
-
const minimist = require('minimist');
|
|
8
|
-
const fs = require('fs-extra');
|
|
9
|
-
const { version } = require('../package.json');
|
|
10
|
-
const { GetPackageList } = require(path.resolve(__dirname, '../utils/package'));
|
|
11
|
-
|
|
12
|
-
const args = process.argv.slice(2);
|
|
13
|
-
const argv = minimist(args, { default: { project: null } });
|
|
14
|
-
const scriptIndex = args.findIndex(x => x === 'build' || x === 'start' || x === 'entry' || x === 'init' || x === 'create');
|
|
15
|
-
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
|
|
16
|
-
|
|
17
|
-
let projectPath = argv.project || process.cwd();
|
|
18
|
-
let templatePath = argv.templatePath
|
|
19
|
-
? path.resolve(projectPath, argv.templatePath)
|
|
20
|
-
: path.resolve(__dirname, '../template');
|
|
21
|
-
|
|
22
|
-
const snackPath = path.resolve(__dirname, '../config/webpack.snack.config.js');
|
|
23
|
-
const indexHTMLPath = path.resolve(__dirname, '../config/webpack.index.config.js');
|
|
24
|
-
const loaderConfigPath = path.resolve(__dirname, '../config/webpack.loader.config.js');
|
|
25
|
-
const devConfigPath = path.resolve(__dirname, '../config/webpack.dev.config.js');
|
|
26
|
-
const entryConfigPath = path.resolve(__dirname, '../config/webpack.entry.config.js');
|
|
27
|
-
|
|
28
|
-
const platform = process.platform;
|
|
29
|
-
const webpackFile = platform === 'win32' ? 'webpack.cmd' : 'webpack';
|
|
30
|
-
const isNpm = fs.existsSync(path.resolve(process.cwd(), './node_modules/.bin/', webpackFile));
|
|
31
|
-
|
|
32
|
-
// ─── 语言检测 ─────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
function detectLang() {
|
|
35
|
-
const env = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || process.env.LC_MESSAGES || '';
|
|
36
|
-
return env.toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const lang = detectLang();
|
|
40
|
-
const t = {
|
|
41
|
-
// init
|
|
42
|
-
initTitle: { zh: ' Snack 项目初始化', en: ' Snack Project Init' },
|
|
43
|
-
projectName: { zh: '项目名称', en: 'Project name' },
|
|
44
|
-
description: { zh: '项目描述', en: 'Description' },
|
|
45
|
-
author: { zh: '作者', en: 'Author' },
|
|
46
|
-
debugServers: { zh: '调试服务器(多个用逗号分隔)', en: 'Debug servers (comma separated)' },
|
|
47
|
-
entryPageId: { zh: '入口页面 ID', en: 'Entry page id' },
|
|
48
|
-
entryPageType: { zh: '入口页面类型', en: 'Entry page type' },
|
|
49
|
-
reactVersion: { zh: 'React 版本', en: 'React version' },
|
|
50
|
-
projectCreated: { zh: ' 项目已创建:', en: ' Project created at:' },
|
|
51
|
-
nextSteps: { zh: ' 后续步骤:', en: ' Next steps:' },
|
|
52
|
-
// create
|
|
53
|
-
createTitle: { zh: ' Snack 模块创建', en: ' Snack Module Create' },
|
|
54
|
-
moduleName: { zh: '模块名称', en: 'Module name' },
|
|
55
|
-
moduleDirName: { zh: '模块目录名(PascalCase)', en: 'Module directory name (PascalCase)' },
|
|
56
|
-
withSetting: { zh: '生成配置模块?(y/n)', en: 'Generate setting module? (y/n)' },
|
|
57
|
-
moduleCreated: { zh: ' 模块已创建:', en: ' Module created:' },
|
|
58
|
-
filesGenerated: { zh: ' 已生成文件:', en: ' Files generated:' },
|
|
59
|
-
// errors
|
|
60
|
-
errNoPkg: { zh: '错误:当前目录没有 package.json,请在 snack 工程根目录下执行该命令', en: 'Error: No package.json found. Run this command in a snack project root.' },
|
|
61
|
-
errDirRequired: { zh: '错误:模块目录名不能为空', en: 'Error: Module directory name is required' },
|
|
62
|
-
errDirExists: { zh: '错误:模块目录已存在:', en: 'Error: Module directory already exists:' },
|
|
63
|
-
errNameConflict: { zh: '请修改 snack-cli package.json "name" 的默认名称,该名称将在部署时作为分类目录', en: 'Please change the default "name" in package.json. It will be used as a category directory during deployment.' },
|
|
64
|
-
errNoEntry: { zh: 'package.json 未找到 snack.entry 配置项', en: 'snack.entry config not found in package.json' },
|
|
65
|
-
errTempDir: { zh: '临时目录创建失败', en: 'Failed to create temp directory' },
|
|
66
|
-
errIntegrity: { zh: '完整性校验失败', en: 'Integrity check failed' },
|
|
67
|
-
// build logs
|
|
68
|
-
clearOutputDir: { zh: '清理输出目录', en: 'clear output dir' },
|
|
69
|
-
clearOutputDirEnd: { zh: '清理完成', en: 'clear output dir end' },
|
|
70
|
-
devServerRunning: { zh: 'Snack 开发服务器运行中...', en: 'Snack DevServer Runing...' },
|
|
71
|
-
createLoaderAndHTML: { zh: '生成 loader.js 和入口 HTML(并行)...', en: 'create loader.js & entry HTML (parallel) ...' },
|
|
72
|
-
createLoaderAndHTMLEnd: { zh: '生成完成', en: 'create loader.js & entry HTML end' },
|
|
73
|
-
createSnack: { zh: '生成 snack 模块...', en: 'create snack ...' },
|
|
74
|
-
createSnackEnd: { zh: '生成完成', en: 'create snack end' },
|
|
75
|
-
createEntry: { zh: '生成入口页...', en: 'create entry ...' },
|
|
76
|
-
integrityPass: { zh: '完整性校验通过', en: 'Integrity check passed' },
|
|
77
|
-
integrityFileMissing: { zh: '完整性校验:文件缺失', en: 'snack check: file is missing' },
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
function i(key) {
|
|
81
|
-
return t[key][lang];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ─── 交互式提问工具 ───────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
function prompt(rl, question, defaultValue) {
|
|
87
|
-
return new Promise(resolve => {
|
|
88
|
-
const hint = defaultValue !== undefined ? ` (${defaultValue})` : '';
|
|
89
|
-
rl.question(`${question}${hint}: `, answer => {
|
|
90
|
-
const val = answer.trim();
|
|
91
|
-
resolve(val === '' && defaultValue !== undefined ? defaultValue : val);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function select(rl, question, options, defaultValue) {
|
|
97
|
-
return new Promise(resolve => {
|
|
98
|
-
const hint = options.map((o, i) => `${i + 1}) ${o}`).join(' ');
|
|
99
|
-
const defaultHint = options.includes(defaultValue) ? defaultValue : options[0];
|
|
100
|
-
rl.question(`${question} [${hint}] (${defaultHint}): `, answer => {
|
|
101
|
-
const val = answer.trim();
|
|
102
|
-
if (val === '') return resolve(defaultHint);
|
|
103
|
-
const num = parseInt(val, 10);
|
|
104
|
-
if (!isNaN(num) && num >= 1 && num <= options.length) return resolve(options[num - 1]);
|
|
105
|
-
if (options.includes(val)) return resolve(val);
|
|
106
|
-
resolve(defaultHint);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ─── init 命令:初始化新 snack 工程 ──────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
async function runInit() {
|
|
114
|
-
const targetDir = argv._[1] ? path.resolve(process.cwd(), argv._[1]) : process.cwd();
|
|
115
|
-
const dirName = path.basename(targetDir);
|
|
116
|
-
|
|
117
|
-
console.log(`\n${i('initTitle')}\n`);
|
|
118
|
-
|
|
119
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
120
|
-
|
|
121
|
-
const name = await prompt(rl, i('projectName'), dirName);
|
|
122
|
-
const description = await prompt(rl, i('description'), '');
|
|
123
|
-
const author = await prompt(rl, i('author'), '');
|
|
124
|
-
const debugInput = await prompt(rl, i('debugServers'), 'http://127.0.0.1:3000');
|
|
125
|
-
const entryId = await prompt(rl, i('entryPageId'), name);
|
|
126
|
-
const entryType = await prompt(rl, i('entryPageType'), 'portal');
|
|
127
|
-
const reactVersion = await select(rl, i('reactVersion'), ['17', '18', '19'], '19');
|
|
128
|
-
|
|
129
|
-
rl.close();
|
|
130
|
-
|
|
131
|
-
const debugServers = debugInput.split(',').map(s => s.trim()).filter(Boolean);
|
|
132
|
-
|
|
133
|
-
const reactVersionMap = {
|
|
134
|
-
'17': { react: '^17.0.0', reactDom: '^17.0.0', typesReact: '^17.0.0', typesReactDom: '^17.0.0' },
|
|
135
|
-
'18': { react: '^18.0.0', reactDom: '^18.0.0', typesReact: '^18.0.0', typesReactDom: '^18.0.0' },
|
|
136
|
-
'19': { react: '^19.0.0', reactDom: '^19.0.0', typesReact: '^19.0.0', typesReactDom: '^19.0.0' },
|
|
137
|
-
};
|
|
138
|
-
const reactDeps = reactVersionMap[reactVersion];
|
|
139
|
-
|
|
140
|
-
// ── package.json ─────────────────────────────────────────────────────────
|
|
141
|
-
const pkg = {
|
|
142
|
-
name,
|
|
143
|
-
version: '1.0.0',
|
|
144
|
-
description,
|
|
145
|
-
scripts: {
|
|
146
|
-
start: 'snack-cli start',
|
|
147
|
-
build: 'snack-cli build',
|
|
148
|
-
entry: 'snack-cli entry'
|
|
149
|
-
},
|
|
150
|
-
input: './src/package',
|
|
151
|
-
output: './dist',
|
|
152
|
-
snack: {
|
|
153
|
-
externals: {},
|
|
154
|
-
buildIgnore: [],
|
|
155
|
-
devPackage: [],
|
|
156
|
-
entry: {
|
|
157
|
-
name: 'entry',
|
|
158
|
-
id: entryId,
|
|
159
|
-
type: entryType,
|
|
160
|
-
title: '',
|
|
161
|
-
favicon: '',
|
|
162
|
-
mobile: { id: `${entryId}_mobile`, type: entryType }
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
author,
|
|
166
|
-
license: 'ISC',
|
|
167
|
-
dependencies: {
|
|
168
|
-
'@snack-kit/core': '^0.3.0',
|
|
169
|
-
'react': reactDeps.react,
|
|
170
|
-
'react-dom': reactDeps.reactDom
|
|
171
|
-
},
|
|
172
|
-
devDependencies: {
|
|
173
|
-
'@types/react': reactDeps.typesReact,
|
|
174
|
-
'@types/react-dom': reactDeps.typesReactDom
|
|
175
|
-
},
|
|
176
|
-
dev: { debug: debugServers }
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// ── tsconfig.json ─────────────────────────────────────────────────────────
|
|
180
|
-
const tsconfig = {
|
|
181
|
-
compilerOptions: {
|
|
182
|
-
target: 'es5',
|
|
183
|
-
module: 'es6',
|
|
184
|
-
outDir: './dist/',
|
|
185
|
-
baseUrl: '.',
|
|
186
|
-
experimentalDecorators: true,
|
|
187
|
-
allowJs: true,
|
|
188
|
-
skipLibCheck: true,
|
|
189
|
-
esModuleInterop: true,
|
|
190
|
-
allowSyntheticDefaultImports: true,
|
|
191
|
-
strict: true,
|
|
192
|
-
forceConsistentCasingInFileNames: true,
|
|
193
|
-
moduleResolution: 'node',
|
|
194
|
-
resolveJsonModule: true,
|
|
195
|
-
isolatedModules: true,
|
|
196
|
-
jsx: 'react-jsx'
|
|
197
|
-
},
|
|
198
|
-
include: ['src'],
|
|
199
|
-
exclude: ['node_modules', 'dist']
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
203
|
-
const gitignore = `node_modules
|
|
204
|
-
dist
|
|
205
|
-
.snack
|
|
206
|
-
*.local
|
|
207
|
-
.DS_Store
|
|
208
|
-
`;
|
|
209
|
-
|
|
210
|
-
// ── src/env.d.ts ──────────────────────────────────────────────────────────
|
|
211
|
-
const envDts = `declare module '*.svg'
|
|
212
|
-
declare module '*.png'
|
|
213
|
-
declare module '*.jpg'
|
|
214
|
-
declare module '*.jpeg'
|
|
215
|
-
declare module '*.gif'
|
|
216
|
-
declare module '*.bmp'
|
|
217
|
-
`;
|
|
218
|
-
|
|
219
|
-
// ── 写入文件 ───────────────────────────────────────────────────────────────
|
|
220
|
-
await fs.ensureDir(targetDir);
|
|
221
|
-
await fs.outputJSON(path.join(targetDir, 'package.json'), pkg, { spaces: 4 });
|
|
222
|
-
await fs.outputJSON(path.join(targetDir, 'tsconfig.json'), tsconfig, { spaces: 4 });
|
|
223
|
-
await fs.outputFile(path.join(targetDir, '.gitignore'), gitignore);
|
|
224
|
-
await fs.ensureDir(path.join(targetDir, 'src/package'));
|
|
225
|
-
await fs.outputFile(path.join(targetDir, 'src/env.d.ts'), envDts);
|
|
226
|
-
|
|
227
|
-
console.log(`
|
|
228
|
-
${i('projectCreated')} ${targetDir}
|
|
229
|
-
|
|
230
|
-
${i('nextSteps')}
|
|
231
|
-
${targetDir !== process.cwd() ? `cd ${argv._[1]}\n ` : ''}npm install
|
|
232
|
-
npm run start
|
|
233
|
-
`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ─── create 命令:在当前工程创建新模块模版 ────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
async function runCreate() {
|
|
239
|
-
const pkgPath = path.resolve(projectPath, 'package.json');
|
|
240
|
-
if (!fs.existsSync(pkgPath)) {
|
|
241
|
-
console.error(i('errNoPkg'));
|
|
242
|
-
process.exit(1);
|
|
243
|
-
}
|
|
244
|
-
const projectPkg = fs.readJSONSync(pkgPath);
|
|
245
|
-
const packageDir = path.resolve(projectPath, projectPkg.input || 'src/package');
|
|
246
|
-
|
|
247
|
-
console.log(`\n${i('createTitle')}\n`);
|
|
248
|
-
|
|
249
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
250
|
-
|
|
251
|
-
const moduleName = await prompt(rl, i('moduleName'), lang === 'zh' ? '新模块' : 'New Module');
|
|
252
|
-
let dirName = await prompt(rl, i('moduleDirName'), '');
|
|
253
|
-
const description = await prompt(rl, i('description'), '');
|
|
254
|
-
const author = await prompt(rl, i('author'), projectPkg.author || '');
|
|
255
|
-
const withSetting = (await prompt(rl, i('withSetting'), 'y')).toLowerCase() === 'y';
|
|
256
|
-
|
|
257
|
-
rl.close();
|
|
258
|
-
|
|
259
|
-
if (!dirName) {
|
|
260
|
-
console.error(i('errDirRequired'));
|
|
261
|
-
process.exit(1);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
dirName = dirName.charAt(0).toUpperCase() + dirName.slice(1);
|
|
265
|
-
const moduleDir = path.join(packageDir, dirName);
|
|
266
|
-
const className = dirName;
|
|
267
|
-
|
|
268
|
-
if (fs.existsSync(moduleDir)) {
|
|
269
|
-
console.error(`${i('errDirExists')} ${moduleDir}`);
|
|
270
|
-
process.exit(1);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ── snack.json ────────────────────────────────────────────────────────────
|
|
274
|
-
const snackJson = {
|
|
275
|
-
name: moduleName,
|
|
276
|
-
version: '0.0.0.1',
|
|
277
|
-
author,
|
|
278
|
-
description
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
// ── index.ts ──────────────────────────────────────────────────────────────
|
|
282
|
-
const indexTs = `export * from './src/index';
|
|
283
|
-
${withSetting ? "export * from './src/setting';\n" : ''}`;
|
|
284
|
-
|
|
285
|
-
// ── src/index.tsx ─────────────────────────────────────────────────────────
|
|
286
|
-
const indexTsx = `import React from 'react';
|
|
287
|
-
import { Snack, SnackData } from '@snack-kit/core';
|
|
288
|
-
import './style/index.scss';
|
|
289
|
-
|
|
290
|
-
interface Props extends SnackData {}
|
|
291
|
-
|
|
292
|
-
export class ${className} extends Snack {
|
|
293
|
-
constructor(data: Props = {}) {
|
|
294
|
-
super(data);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
public $component(props: Props): React.ReactNode {
|
|
298
|
-
return (
|
|
299
|
-
<div className={'${className.toLowerCase()}'}>
|
|
300
|
-
{/* TODO */}
|
|
301
|
-
</div>
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export default ${className};
|
|
307
|
-
`;
|
|
308
|
-
|
|
309
|
-
// ── src/setting.tsx ───────────────────────────────────────────────────────
|
|
310
|
-
const settingTsx = `import React from 'react';
|
|
311
|
-
import { SnackSetting } from '@snack-kit/core';
|
|
312
|
-
import Main from './index';
|
|
313
|
-
import './style/setting.scss';
|
|
314
|
-
|
|
315
|
-
interface Props {}
|
|
316
|
-
|
|
317
|
-
export class ${className}Setting extends SnackSetting {
|
|
318
|
-
constructor(public main: Main, public data: Props) {
|
|
319
|
-
super(data);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
public $component(): React.ReactNode {
|
|
323
|
-
return <></>;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
`;
|
|
327
|
-
|
|
328
|
-
// ── 样式文件 ───────────────────────────────────────────────────────────────
|
|
329
|
-
const indexScss = `.${className.toLowerCase()} {\n}\n`;
|
|
330
|
-
const settingScss = `.${className.toLowerCase()}-setting {\n}\n`;
|
|
331
|
-
|
|
332
|
-
// ── 写入文件 ───────────────────────────────────────────────────────────────
|
|
333
|
-
await fs.outputJSON(path.join(moduleDir, 'snack.json'), snackJson, { spaces: 4 });
|
|
334
|
-
await fs.outputFile(path.join(moduleDir, 'index.ts'), indexTs);
|
|
335
|
-
await fs.outputFile(path.join(moduleDir, 'src/index.tsx'), indexTsx);
|
|
336
|
-
await fs.outputFile(path.join(moduleDir, 'src/style/index.scss'), indexScss);
|
|
337
|
-
|
|
338
|
-
if (withSetting) {
|
|
339
|
-
await fs.outputFile(path.join(moduleDir, 'src/setting.tsx'), settingTsx);
|
|
340
|
-
await fs.outputFile(path.join(moduleDir, 'src/style/setting.scss'), settingScss);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
console.log(`
|
|
344
|
-
${i('moduleCreated')} ${moduleDir}
|
|
345
|
-
|
|
346
|
-
${i('filesGenerated')}
|
|
347
|
-
${dirName}/snack.json
|
|
348
|
-
${dirName}/index.ts
|
|
349
|
-
${dirName}/src/index.tsx
|
|
350
|
-
${dirName}/src/style/index.scss${withSetting ? `
|
|
351
|
-
${dirName}/src/setting.tsx
|
|
352
|
-
${dirName}/src/style/setting.scss` : ''}
|
|
353
|
-
`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// ─── webpack 构建命令 ─────────────────────────────────────────────────────────
|
|
357
|
-
|
|
358
|
-
function execWebpack(args) {
|
|
359
|
-
return new Promise(resolve => {
|
|
360
|
-
const ps = isNpm
|
|
361
|
-
? spawn('webpack', args, { stdio: 'inherit', shell: true })
|
|
362
|
-
: spawn(path.resolve(__dirname, '../node_modules/.bin/', webpackFile), args, { stdio: 'inherit' });
|
|
363
|
-
ps.on('error', e => { console.log(e); resolve(false); });
|
|
364
|
-
ps.on('exit', () => resolve(true));
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function clearOutput(output) {
|
|
369
|
-
fs.removeSync(output);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
async function check(output) {
|
|
373
|
-
try {
|
|
374
|
-
const list = await fs.readdir(output);
|
|
375
|
-
for (const dirname of list) {
|
|
376
|
-
if (dirname === '__common__') continue;
|
|
377
|
-
const snackjson = path.join(output, dirname, 'snack.json');
|
|
378
|
-
if (!await fs.exists(snackjson)) continue;
|
|
379
|
-
const index = path.join(output, dirname, 'index.js');
|
|
380
|
-
if (!await fs.exists(index)) {
|
|
381
|
-
console.error(`${i('integrityFileMissing')}: ${index}`);
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
} catch (err) {
|
|
386
|
-
console.error('snack check: ', err);
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
console.log(i('integrityPass'));
|
|
390
|
-
return true;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function run(script) {
|
|
394
|
-
const projectPackageJSON = fs.readJSONSync(path.resolve(projectPath, 'package.json'));
|
|
395
|
-
if (projectPackageJSON.name === 'snack-cli') {
|
|
396
|
-
throw i('errNameConflict');
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
process.env.SNACK_CLI_VERSION = version;
|
|
400
|
-
process.env.RUN_SCRIPT = script;
|
|
401
|
-
process.env.SNACK_SCRIPTS_PATH = __dirname;
|
|
402
|
-
process.env.PROJECT_COMMON_NAME = '__common__';
|
|
403
|
-
process.env.PROJECT_PACKAGE_JSON = JSON.stringify(projectPackageJSON);
|
|
404
|
-
process.env.PROJECT_TYPE = projectPackageJSON.name;
|
|
405
|
-
process.env.PROJECT_PATH = projectPath;
|
|
406
|
-
process.env.PROJECT_PACKAGE_PATH = path.resolve(projectPath, projectPackageJSON.input || 'src/package');
|
|
407
|
-
process.env.PROJECT_TEMP_PATH = path.resolve(projectPath, '.snack');
|
|
408
|
-
process.env.PROJECT_TEMPLATE_PATH = path.join(process.env.PROJECT_TEMP_PATH, 'template');
|
|
409
|
-
|
|
410
|
-
await fs.remove(process.env.PROJECT_TEMPLATE_PATH);
|
|
411
|
-
const err = await fs.copy(templatePath, process.env.PROJECT_TEMPLATE_PATH);
|
|
412
|
-
if (err) {
|
|
413
|
-
console.error(i('errTempDir'));
|
|
414
|
-
throw err;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
process.env.PROJECT_SNACK_CONFIG = JSON.stringify(projectPackageJSON.snack || {});
|
|
418
|
-
|
|
419
|
-
if (script === 'entry') {
|
|
420
|
-
if (!projectPackageJSON.snack?.entry) throw i('errNoEntry');
|
|
421
|
-
process.env.RUN_MODE = 'production';
|
|
422
|
-
process.env.PROJECT_OUT_PATH = path.resolve(projectPath, projectPackageJSON.snack.entry.name || 'entry');
|
|
423
|
-
console.log(i('createEntry'));
|
|
424
|
-
await execWebpack(['--mode=production', `--config=${entryConfigPath}`]);
|
|
425
|
-
} else {
|
|
426
|
-
if (script === 'start') {
|
|
427
|
-
process.env.RUN_MODE = 'development';
|
|
428
|
-
process.env.PROJECT_OUT_PATH = path.resolve(process.env.PROJECT_TEMP_PATH, 'temp');
|
|
429
|
-
} else {
|
|
430
|
-
process.env.RUN_MODE = 'production';
|
|
431
|
-
process.env.PROJECT_OUT_PATH = path.resolve(projectPath, projectPackageJSON.output || 'dist', projectPackageJSON.name);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
process.env.PROJECT_PACKAGE_LIST = JSON.stringify(GetPackageList(process.env.PROJECT_PACKAGE_PATH));
|
|
435
|
-
|
|
436
|
-
console.log(`${i('clearOutputDir')} "${process.env.PROJECT_OUT_PATH}" ...`);
|
|
437
|
-
clearOutput(process.env.PROJECT_OUT_PATH);
|
|
438
|
-
console.log(i('clearOutputDirEnd'));
|
|
439
|
-
|
|
440
|
-
if (script === 'start') {
|
|
441
|
-
console.log(i('devServerRunning'));
|
|
442
|
-
await execWebpack(['serve', '--mode=development', `--config=${devConfigPath}`]);
|
|
443
|
-
} else {
|
|
444
|
-
console.log(i('createLoaderAndHTML'));
|
|
445
|
-
await Promise.all([
|
|
446
|
-
execWebpack(['--mode=production', `--config=${loaderConfigPath}`]),
|
|
447
|
-
execWebpack(['--mode=production', `--config=${indexHTMLPath}`])
|
|
448
|
-
]);
|
|
449
|
-
console.log(i('createLoaderAndHTMLEnd'));
|
|
450
|
-
console.log(i('createSnack'));
|
|
451
|
-
await execWebpack(['--mode=production', `--config=${snackPath}`]);
|
|
452
|
-
console.log(i('createSnackEnd'));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (process.env.RUN_MODE !== 'development' && !await check(process.env.PROJECT_OUT_PATH)) {
|
|
456
|
-
fs.removeSync(process.env.PROJECT_OUT_PATH);
|
|
457
|
-
throw i('errIntegrity');
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
console.log(`
|
|
462
|
-
_____ __ ________ ____
|
|
463
|
-
/ ___/____ ____ ______/ /__ / ____/ / / _/
|
|
464
|
-
\\__ \\/ __ \\/ __ \`/ ___/ //_/ / / / / / /
|
|
465
|
-
___/ / / / / /_/ / /__/ ,< / /___/ /____/ /
|
|
466
|
-
/____/_/ /_/\\__,_/\\___/_/|_| \\____/_____/___/ v.${version}
|
|
467
|
-
`);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// ─── 入口路由 ─────────────────────────────────────────────────────────────────
|
|
471
|
-
|
|
472
|
-
if (script === 'init') {
|
|
473
|
-
runInit().catch(err => { console.error(err); process.exit(1); });
|
|
474
|
-
} else if (script === 'create') {
|
|
475
|
-
runCreate().catch(err => { console.error(err); process.exit(1); });
|
|
476
|
-
} else {
|
|
477
|
-
run(script).catch(err => { console.error(err); process.exit(1); });
|
|
478
|
-
}
|