create-widget 0.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/.editorconfig +13 -0
- package/.idea/create-widget.iml +9 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/jpa-buddy.xml +6 -0
- package/.idea/misc.xml +8 -0
- package/.idea/modules.xml +8 -0
- package/.idea/prettier.xml +6 -0
- package/.idea/vcs.xml +6 -0
- package/lib/index.js +166 -0
- package/package.json +48 -0
- package/src/index.ts +144 -0
- package/src/utils/FileUtils.ts +33 -0
- package/src/utils/deepMerge.ts +26 -0
- package/src/utils/directoryTraverse.ts +35 -0
- package/src/utils/getCommand.ts +13 -0
- package/src/utils/renderTemplate.ts +88 -0
- package/src/utils/sortDependencies.ts +22 -0
- package/template/.vscode/extensions.json +3 -0
- package/template/README.md +40 -0
- package/template/env.d.ts +1 -0
- package/template/index.html +13 -0
- package/template/package.json +31 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/preview_clock.png +0 -0
- package/template/public/widget.json +50 -0
- package/template/src/App.vue +9 -0
- package/template/src/assets/main.css +3 -0
- package/template/src/main.ts +12 -0
- package/template/src/router/index.ts +11 -0
- package/template/src/widgets/clock/Clock.widget.ts +32 -0
- package/template/src/widgets/clock/ClockConfigView.vue +38 -0
- package/template/src/widgets/clock/ClockWidgetRoutes.ts +28 -0
- package/template/src/widgets/clock/ClockWidgetView.vue +34 -0
- package/template/src/widgets/clock/model/ClockModel.ts +5 -0
- package/template/src/widgets/widget-router.ts +8 -0
- package/template/tsconfig.app.json +13 -0
- package/template/tsconfig.json +11 -0
- package/template/tsconfig.node.json +17 -0
- package/template/vite.config.ts +14 -0
- package/template/widget.package.ts +20 -0
- package/test/index.test.ts +24 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +6 -0
- package/widget-test/.vscode/extensions.json +3 -0
- package/widget-test/README.md +40 -0
- package/widget-test/env.d.ts +1 -0
- package/widget-test/index.html +13 -0
- package/widget-test/package.json +30 -0
- package/widget-test/public/favicon.ico +0 -0
- package/widget-test/public/preview_clock.png +0 -0
- package/widget-test/public/widget.json +50 -0
- package/widget-test/src/App.vue +9 -0
- package/widget-test/src/assets/main.css +3 -0
- package/widget-test/src/main.ts +12 -0
- package/widget-test/src/router/index.ts +11 -0
- package/widget-test/src/widgets/clock/Clock.widget.ts +32 -0
- package/widget-test/src/widgets/clock/ClockConfigView.vue +38 -0
- package/widget-test/src/widgets/clock/ClockWidgetRoutes.ts +28 -0
- package/widget-test/src/widgets/clock/ClockWidgetView.vue +34 -0
- package/widget-test/src/widgets/clock/model/ClockModel.ts +5 -0
- package/widget-test/src/widgets/widget-router.ts +8 -0
- package/widget-test/tsconfig.app.json +13 -0
- package/widget-test/tsconfig.json +11 -0
- package/widget-test/tsconfig.node.json +17 -0
- package/widget-test/vite.config.ts +14 -0
- package/widget-test/widget.package.ts +20 -0
- package/widget-test/yarn.lock +1399 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="JAVA_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
+
<exclude-output />
|
|
5
|
+
<content url="file://$MODULE_DIR$" />
|
|
6
|
+
<orderEntry type="inheritedJdk" />
|
|
7
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
+
</component>
|
|
9
|
+
</module>
|
package/.idea/misc.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/create-widget.iml" filepath="$PROJECT_DIR$/.idea/create-widget.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
package/.idea/vcs.xml
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import fs3 from "fs";
|
|
3
|
+
import * as process from "process";
|
|
4
|
+
import gradient from "gradient-string";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
import minimist from "minimist";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
|
|
10
|
+
// src/utils/directoryTraverse.ts
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
function postOrderDirectoryTraverse(dir, dirCallback, fileCallback) {
|
|
14
|
+
for (const filename of fs.readdirSync(dir)) {
|
|
15
|
+
if (filename === ".git") {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const fullpath = path.resolve(dir, filename);
|
|
19
|
+
if (fs.lstatSync(fullpath).isDirectory()) {
|
|
20
|
+
postOrderDirectoryTraverse(fullpath, dirCallback, fileCallback);
|
|
21
|
+
dirCallback(fullpath);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
fileCallback(fullpath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/utils/getCommand.ts
|
|
29
|
+
function getCommand(packageManager, scriptName, args) {
|
|
30
|
+
if (scriptName === "install") {
|
|
31
|
+
return packageManager === "yarn" ? "yarn" : `${packageManager} install`;
|
|
32
|
+
}
|
|
33
|
+
if (args) {
|
|
34
|
+
return packageManager === "npm" ? `npm run ${scriptName} -- ${args}` : `${packageManager} ${scriptName} ${args}`;
|
|
35
|
+
} else {
|
|
36
|
+
return packageManager === "npm" ? `npm run ${scriptName}` : `${packageManager} ${scriptName}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/index.ts
|
|
41
|
+
import { fileURLToPath } from "url";
|
|
42
|
+
|
|
43
|
+
// src/utils/FileUtils.ts
|
|
44
|
+
import * as fs2 from "fs";
|
|
45
|
+
import { copy, ensureDir } from "fs-extra";
|
|
46
|
+
var FileUtils = class {
|
|
47
|
+
static async copyFolderRecursive(src, dest) {
|
|
48
|
+
try {
|
|
49
|
+
await ensureDir(dest);
|
|
50
|
+
const items = await fs2.promises.readdir(src);
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
const srcPath = `${src}/${item}`;
|
|
53
|
+
const destPath = `${dest}/${item}`;
|
|
54
|
+
const stats = await fs2.promises.stat(srcPath);
|
|
55
|
+
if (stats.isDirectory()) {
|
|
56
|
+
await this.copyFolderRecursive(srcPath, destPath);
|
|
57
|
+
} else {
|
|
58
|
+
await copy(srcPath, destPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`Error copying folder: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/index.ts
|
|
68
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
69
|
+
var __dirname = path2.dirname(__filename);
|
|
70
|
+
function canSkipEmptying(dir) {
|
|
71
|
+
if (!fs3.existsSync(dir)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const files = fs3.readdirSync(dir);
|
|
75
|
+
if (files.length === 0) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (files.length === 1 && files[0] === ".git") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function emptyDir(dir) {
|
|
84
|
+
if (!fs3.existsSync(dir)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
postOrderDirectoryTraverse(
|
|
88
|
+
dir,
|
|
89
|
+
(dir2) => fs3.rmdirSync(dir2),
|
|
90
|
+
(file) => fs3.unlinkSync(file)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
async function init() {
|
|
94
|
+
console.log();
|
|
95
|
+
let defaultBanner = "Widget.js - The Desktop Widget Framework";
|
|
96
|
+
const gradientBanner = gradient([
|
|
97
|
+
{ color: "#42d392", pos: 0 },
|
|
98
|
+
{ color: "#42d392", pos: 0.1 },
|
|
99
|
+
{ color: "#647eff", pos: 1 }
|
|
100
|
+
])(defaultBanner);
|
|
101
|
+
console.log(process.stdout.isTTY && process.stdout.getColorDepth() > 8 ? gradientBanner : defaultBanner);
|
|
102
|
+
console.log();
|
|
103
|
+
const argv2 = minimist(process.argv.slice(2), {
|
|
104
|
+
alias: {
|
|
105
|
+
typescript: ["ts"],
|
|
106
|
+
"with-tests": ["tests"],
|
|
107
|
+
router: ["vue-router"]
|
|
108
|
+
},
|
|
109
|
+
string: ["_"],
|
|
110
|
+
// all arguments are treated as booleans
|
|
111
|
+
boolean: true
|
|
112
|
+
});
|
|
113
|
+
let targetDir = argv2._[0];
|
|
114
|
+
const defaultProjectName = !targetDir ? "widget-project" : targetDir;
|
|
115
|
+
const forceOverwrite = argv2.force;
|
|
116
|
+
const cwd2 = process.cwd();
|
|
117
|
+
let result = {};
|
|
118
|
+
result = await prompts(
|
|
119
|
+
[
|
|
120
|
+
{
|
|
121
|
+
name: "projectName",
|
|
122
|
+
type: targetDir ? null : "text",
|
|
123
|
+
message: "Project name:",
|
|
124
|
+
initial: defaultProjectName,
|
|
125
|
+
onState: (state) => targetDir = String(state.value).trim() || defaultProjectName
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "shouldOverwrite",
|
|
129
|
+
type: () => canSkipEmptying(targetDir) || forceOverwrite ? null : "confirm",
|
|
130
|
+
message: () => {
|
|
131
|
+
const dirForPrompt = targetDir === "." ? "Current directory" : `Target directory "${targetDir}"`;
|
|
132
|
+
return `${dirForPrompt} is not empty. Remove existing files and continue?`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
{
|
|
137
|
+
onCancel: () => {
|
|
138
|
+
throw new Error(chalk.red("\u2716") + " Operation cancelled");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
const { projectName, shouldOverwrite = argv2.force } = result;
|
|
143
|
+
const root = path2.join(cwd2, targetDir);
|
|
144
|
+
if (fs3.existsSync(root) && shouldOverwrite) {
|
|
145
|
+
emptyDir(root);
|
|
146
|
+
} else if (!fs3.existsSync(root)) {
|
|
147
|
+
fs3.mkdirSync(root);
|
|
148
|
+
}
|
|
149
|
+
console.log(`
|
|
150
|
+
Scaffolding project in ${root}...`);
|
|
151
|
+
const templateRoot = path2.join(__dirname, "../template");
|
|
152
|
+
await FileUtils.copyFolderRecursive(templateRoot, root);
|
|
153
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
154
|
+
const packageManager = /pnpm/.test(userAgent) ? "pnpm" : /yarn/.test(userAgent) ? "yarn" : "npm";
|
|
155
|
+
console.log(`
|
|
156
|
+
Done. Now run:
|
|
157
|
+
`);
|
|
158
|
+
if (root !== cwd2) {
|
|
159
|
+
const cdProjectName = path2.relative(cwd2, root);
|
|
160
|
+
console.log(` ${chalk.bold(chalk.green(`cd ${cdProjectName.includes(" ") ? `"${cdProjectName}"` : cdProjectName}`))}`);
|
|
161
|
+
}
|
|
162
|
+
console.log(` ${chalk.bold(chalk.green(getCommand(packageManager, "install")))}`);
|
|
163
|
+
}
|
|
164
|
+
init().catch((e) => {
|
|
165
|
+
console.error(e);
|
|
166
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-widget",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "lib/index.js",
|
|
5
|
+
"author": "Neo Fu",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"bin": {
|
|
9
|
+
"widget": "lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": "^12.0.0 || >= 14.0.0"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"chalk": "^5.3.0",
|
|
20
|
+
"consola": "^2.15.3",
|
|
21
|
+
"ejs": "^3.1.8",
|
|
22
|
+
"fs-extra": "^11.2.0",
|
|
23
|
+
"gradient-string": "^2.0.2",
|
|
24
|
+
"minimist": "^1.2.8",
|
|
25
|
+
"prompts": "^2.4.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@tsconfig/node18": "^18.2.2",
|
|
29
|
+
"@types/gradient-string": "^1.1.2",
|
|
30
|
+
"@types/ejs": "latest",
|
|
31
|
+
"@types/fs-extra": "^11.0.4",
|
|
32
|
+
"@types/minimist": "^1.2.5",
|
|
33
|
+
"@types/node": "^18.11.13",
|
|
34
|
+
"@types/prompts": "^2.4.9",
|
|
35
|
+
"ts-loader": "^9.4.1",
|
|
36
|
+
"ts-node": "^10.9.1",
|
|
37
|
+
"tsup": "^6.5.0",
|
|
38
|
+
"typescript": "^5.2.2",
|
|
39
|
+
"vitest": "^0.34.6"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "rimraf ./lib/ && tsup-node src/index.ts --format esm",
|
|
43
|
+
"watch": "tsup-node src/index.ts --format esm --watch",
|
|
44
|
+
"build:run": "npm run build && npm run create-widget",
|
|
45
|
+
"create-widget": "node ./lib/index.js",
|
|
46
|
+
"pnpm:publish": "npm run build && pnpm publish --no-git-checks"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import * as process from 'process'
|
|
3
|
+
import gradient from 'gradient-string'
|
|
4
|
+
import prompts from 'prompts'
|
|
5
|
+
import minimist from 'minimist'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
import {postOrderDirectoryTraverse} from './utils/directoryTraverse'
|
|
9
|
+
import getCommand from './utils/getCommand'
|
|
10
|
+
import {fileURLToPath} from 'url'
|
|
11
|
+
import {FileUtils} from "./utils/FileUtils";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
14
|
+
const __dirname = path.dirname(__filename)
|
|
15
|
+
|
|
16
|
+
function canSkipEmptying(dir: string) {
|
|
17
|
+
if (!fs.existsSync(dir)) {
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const files = fs.readdirSync(dir)
|
|
22
|
+
if (files.length === 0) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
if (files.length === 1 && files[0] === '.git') {
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function emptyDir(dir) {
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
postOrderDirectoryTraverse(
|
|
38
|
+
dir,
|
|
39
|
+
dir => fs.rmdirSync(dir),
|
|
40
|
+
file => fs.unlinkSync(file),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function init() {
|
|
45
|
+
console.log()
|
|
46
|
+
let defaultBanner = 'Widget.js - The Desktop Widget Framework'
|
|
47
|
+
const gradientBanner = gradient([
|
|
48
|
+
{color: '#42d392', pos: 0},
|
|
49
|
+
{color: '#42d392', pos: 0.1},
|
|
50
|
+
{color: '#647eff', pos: 1},
|
|
51
|
+
])(defaultBanner)
|
|
52
|
+
console.log(process.stdout.isTTY && process.stdout.getColorDepth() > 8 ? gradientBanner : defaultBanner)
|
|
53
|
+
console.log()
|
|
54
|
+
const argv = minimist(process.argv.slice(2), {
|
|
55
|
+
alias: {
|
|
56
|
+
typescript: ['ts'],
|
|
57
|
+
'with-tests': ['tests'],
|
|
58
|
+
router: ['vue-router'],
|
|
59
|
+
},
|
|
60
|
+
string: ['_'],
|
|
61
|
+
// all arguments are treated as booleans
|
|
62
|
+
boolean: true,
|
|
63
|
+
})
|
|
64
|
+
let targetDir = argv._[0]
|
|
65
|
+
const defaultProjectName = !targetDir ? 'widget-project' : targetDir
|
|
66
|
+
const forceOverwrite = argv.force
|
|
67
|
+
|
|
68
|
+
const cwd = process.cwd()
|
|
69
|
+
let result: {
|
|
70
|
+
projectName?: string
|
|
71
|
+
shouldOverwrite?: boolean
|
|
72
|
+
} = {}
|
|
73
|
+
result = await prompts(
|
|
74
|
+
[
|
|
75
|
+
{
|
|
76
|
+
name: 'projectName',
|
|
77
|
+
type: targetDir ? null : 'text',
|
|
78
|
+
message: 'Project name:',
|
|
79
|
+
initial: defaultProjectName,
|
|
80
|
+
onState: state => (targetDir = String(state.value).trim() || defaultProjectName),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'shouldOverwrite',
|
|
84
|
+
type: () => (canSkipEmptying(targetDir) || forceOverwrite ? null : 'confirm'),
|
|
85
|
+
message: () => {
|
|
86
|
+
const dirForPrompt = targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`
|
|
87
|
+
return `${dirForPrompt} is not empty. Remove existing files and continue?`
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
{
|
|
92
|
+
onCancel: () => {
|
|
93
|
+
throw new Error(chalk.red('✖') + ' Operation cancelled')
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// `initial` won't take effect if the prompt type is null
|
|
99
|
+
// so we still have to assign the default values here
|
|
100
|
+
const {projectName, shouldOverwrite = argv.force} = result
|
|
101
|
+
|
|
102
|
+
const root = path.join(cwd, targetDir)
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(root) && shouldOverwrite) {
|
|
105
|
+
emptyDir(root)
|
|
106
|
+
} else if (!fs.existsSync(root)) {
|
|
107
|
+
fs.mkdirSync(root)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`\nScaffolding project in ${root}...`)
|
|
111
|
+
|
|
112
|
+
const templateRoot = path.join(__dirname, '../template')
|
|
113
|
+
|
|
114
|
+
//复制templateRoot下的文件到root目录下
|
|
115
|
+
await FileUtils.copyFolderRecursive(templateRoot, root)
|
|
116
|
+
|
|
117
|
+
// const callbacks = []
|
|
118
|
+
//
|
|
119
|
+
// const render = function render(templateName) {
|
|
120
|
+
// const templateDir = path.resolve(templateRoot, templateName)
|
|
121
|
+
// renderTemplate(templateDir, root, callbacks)
|
|
122
|
+
// }
|
|
123
|
+
// // 将template里的文件都复制到 root目录下
|
|
124
|
+
// const templateFiles = fs.readdirSync(templateRoot)
|
|
125
|
+
// for (const file of templateFiles) {
|
|
126
|
+
// render(file)
|
|
127
|
+
// }
|
|
128
|
+
|
|
129
|
+
// Instructions:
|
|
130
|
+
// Supported package managers: pnpm > yarn > npm
|
|
131
|
+
const userAgent = process.env.npm_config_user_agent ?? ''
|
|
132
|
+
const packageManager = /pnpm/.test(userAgent) ? 'pnpm' : /yarn/.test(userAgent) ? 'yarn' : 'npm'
|
|
133
|
+
|
|
134
|
+
console.log(`\nDone. Now run:\n`)
|
|
135
|
+
if (root !== cwd) {
|
|
136
|
+
const cdProjectName = path.relative(cwd, root)
|
|
137
|
+
console.log(` ${chalk.bold(chalk.green(`cd ${cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName}`))}`)
|
|
138
|
+
}
|
|
139
|
+
console.log(` ${chalk.bold(chalk.green(getCommand(packageManager, 'install')))}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
init().catch(e => {
|
|
143
|
+
console.error(e)
|
|
144
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import {copy,ensureDir} from "fs-extra";
|
|
3
|
+
|
|
4
|
+
export class FileUtils {
|
|
5
|
+
static async copyFolderRecursive(src: string, dest: string): Promise<void> {
|
|
6
|
+
try {
|
|
7
|
+
// 创建目标文件夹
|
|
8
|
+
await ensureDir(dest)
|
|
9
|
+
|
|
10
|
+
// 获取源文件夹中的所有文件和子文件夹
|
|
11
|
+
const items = await fs.promises.readdir(src)
|
|
12
|
+
|
|
13
|
+
// 遍历每一个文件或子文件夹
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const srcPath = `${src}/${item}`
|
|
16
|
+
const destPath = `${dest}/${item}`
|
|
17
|
+
|
|
18
|
+
// 检查当前项目是文件还是文件夹
|
|
19
|
+
const stats = await fs.promises.stat(srcPath)
|
|
20
|
+
|
|
21
|
+
if (stats.isDirectory()) {
|
|
22
|
+
// 如果是文件夹,递归复制
|
|
23
|
+
await this.copyFolderRecursive(srcPath, destPath)
|
|
24
|
+
} else {
|
|
25
|
+
// 如果是文件,直接复制
|
|
26
|
+
await copy(srcPath, destPath)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`Error copying folder: ${error.message}`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const isObject = (val) => val && typeof val === 'object'
|
|
2
|
+
const mergeArrayWithDedupe = (a, b) => Array.from(new Set([...a, ...b]))
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recursively merge the content of the new object to the existing one
|
|
6
|
+
* @param {Object} target the existing object
|
|
7
|
+
* @param {Object} obj the new object
|
|
8
|
+
*/
|
|
9
|
+
function deepMerge(target, obj) {
|
|
10
|
+
for (const key of Object.keys(obj)) {
|
|
11
|
+
const oldVal = target[key]
|
|
12
|
+
const newVal = obj[key]
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
|
15
|
+
target[key] = mergeArrayWithDedupe(oldVal, newVal)
|
|
16
|
+
} else if (isObject(oldVal) && isObject(newVal)) {
|
|
17
|
+
target[key] = deepMerge(oldVal, newVal)
|
|
18
|
+
} else {
|
|
19
|
+
target[key] = newVal
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return target
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default deepMerge
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function preOrderDirectoryTraverse(dir, dirCallback, fileCallback) {
|
|
5
|
+
for (const filename of fs.readdirSync(dir)) {
|
|
6
|
+
if (filename === '.git') {
|
|
7
|
+
continue
|
|
8
|
+
}
|
|
9
|
+
const fullpath = path.resolve(dir, filename)
|
|
10
|
+
if (fs.lstatSync(fullpath).isDirectory()) {
|
|
11
|
+
dirCallback(fullpath)
|
|
12
|
+
// in case the dirCallback removes the directory entirely
|
|
13
|
+
if (fs.existsSync(fullpath)) {
|
|
14
|
+
preOrderDirectoryTraverse(fullpath, dirCallback, fileCallback)
|
|
15
|
+
}
|
|
16
|
+
continue
|
|
17
|
+
}
|
|
18
|
+
fileCallback(fullpath)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function postOrderDirectoryTraverse(dir, dirCallback, fileCallback) {
|
|
23
|
+
for (const filename of fs.readdirSync(dir)) {
|
|
24
|
+
if (filename === '.git') {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
const fullpath = path.resolve(dir, filename)
|
|
28
|
+
if (fs.lstatSync(fullpath).isDirectory()) {
|
|
29
|
+
postOrderDirectoryTraverse(fullpath, dirCallback, fileCallback)
|
|
30
|
+
dirCallback(fullpath)
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
fileCallback(fullpath)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function getCommand(packageManager: string, scriptName: string, args?: string) {
|
|
2
|
+
if (scriptName === 'install') {
|
|
3
|
+
return packageManager === 'yarn' ? 'yarn' : `${packageManager} install`
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (args) {
|
|
7
|
+
return packageManager === 'npm'
|
|
8
|
+
? `npm run ${scriptName} -- ${args}`
|
|
9
|
+
: `${packageManager} ${scriptName} ${args}`
|
|
10
|
+
} else {
|
|
11
|
+
return packageManager === 'npm' ? `npm run ${scriptName}` : `${packageManager} ${scriptName}`
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import deepMerge from './deepMerge'
|
|
6
|
+
import sortDependencies from './sortDependencies'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders a template folder/file to the file system,
|
|
10
|
+
* by recursively copying all files under the `src` directory,
|
|
11
|
+
* with the following exception:
|
|
12
|
+
* - `_filename` should be renamed to `.filename`
|
|
13
|
+
* - Fields in `package.json` should be recursively merged
|
|
14
|
+
* @param {string} src source filename to copy
|
|
15
|
+
* @param {string} dest destination filename of the copy operation
|
|
16
|
+
*/
|
|
17
|
+
function renderTemplate(src, dest, callbacks) {
|
|
18
|
+
const stats = fs.statSync(src)
|
|
19
|
+
|
|
20
|
+
if (stats.isDirectory()) {
|
|
21
|
+
// skip node_module
|
|
22
|
+
if (path.basename(src) === 'node_modules') {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// if it's a directory, render its subdirectories and files recursively
|
|
27
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
28
|
+
for (const file of fs.readdirSync(src)) {
|
|
29
|
+
renderTemplate(path.resolve(src, file), path.resolve(dest, file), callbacks)
|
|
30
|
+
}
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const filename = path.basename(src)
|
|
35
|
+
|
|
36
|
+
if (filename === 'package.json' && fs.existsSync(dest)) {
|
|
37
|
+
// merge instead of overwriting
|
|
38
|
+
const existing = JSON.parse(fs.readFileSync(dest, 'utf8'))
|
|
39
|
+
const newPackage = JSON.parse(fs.readFileSync(src, 'utf8'))
|
|
40
|
+
const pkg = sortDependencies(deepMerge(existing, newPackage))
|
|
41
|
+
fs.writeFileSync(dest, JSON.stringify(pkg, null, 2) + '\n')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (filename === 'extensions.json' && fs.existsSync(dest)) {
|
|
46
|
+
// merge instead of overwriting
|
|
47
|
+
const existing = JSON.parse(fs.readFileSync(dest, 'utf8'))
|
|
48
|
+
const newExtensions = JSON.parse(fs.readFileSync(src, 'utf8'))
|
|
49
|
+
const extensions = deepMerge(existing, newExtensions)
|
|
50
|
+
fs.writeFileSync(dest, JSON.stringify(extensions, null, 2) + '\n')
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (filename.startsWith('_')) {
|
|
55
|
+
// rename `_file` to `.file`
|
|
56
|
+
dest = path.resolve(path.dirname(dest), filename.replace(/^_/, '.'))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (filename === '_gitignore' && fs.existsSync(dest)) {
|
|
60
|
+
// append to existing .gitignore
|
|
61
|
+
const existing = fs.readFileSync(dest, 'utf8')
|
|
62
|
+
const newGitignore = fs.readFileSync(src, 'utf8')
|
|
63
|
+
fs.writeFileSync(dest, existing + '\n' + newGitignore)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// data file for EJS templates
|
|
68
|
+
if (filename.endsWith('.data.mjs')) {
|
|
69
|
+
// use dest path as key for the data store
|
|
70
|
+
dest = dest.replace(/\.data\.mjs$/, '')
|
|
71
|
+
|
|
72
|
+
// Add a callback to the array for late usage when template files are being processed
|
|
73
|
+
callbacks.push(async (dataStore) => {
|
|
74
|
+
const getData = (await import(pathToFileURL(src).toString())).default
|
|
75
|
+
|
|
76
|
+
// Though current `getData` are all sync, we still retain the possibility of async
|
|
77
|
+
dataStore[dest] = await getData({
|
|
78
|
+
oldData: dataStore[dest] || {}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return // skip copying the data file
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fs.copyFileSync(src, dest)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default renderTemplate
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default function sortDependencies(packageJson) {
|
|
2
|
+
const sorted = {}
|
|
3
|
+
|
|
4
|
+
const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
5
|
+
|
|
6
|
+
for (const depType of depTypes) {
|
|
7
|
+
if (packageJson[depType]) {
|
|
8
|
+
sorted[depType] = {}
|
|
9
|
+
|
|
10
|
+
Object.keys(packageJson[depType])
|
|
11
|
+
.sort()
|
|
12
|
+
.forEach((name) => {
|
|
13
|
+
sorted[depType][name] = packageJson[depType][name]
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
...packageJson,
|
|
20
|
+
...sorted
|
|
21
|
+
}
|
|
22
|
+
}
|