create-routify 1.4.2 → 1.5.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 +13 -3
- package/package.json +5 -7
- package/src/bin.js +38 -4
- package/src/index.js +305 -83
- package/src/utils/index.js +23 -0
- package/src/utils/patcher/test/index.js +112 -0
- package/src/utils/repos.js +131 -0
- package/src/versions404/three/index.js +29 -0
- package/src/versions404/three/utils.js +24 -0
- package/src/versions404/two.js +18 -0
- package/src/utils/prompts.js +0 -10
- package/src/versions/three/index.js +0 -84
- package/src/versions/three/utils.js +0 -11
package/README.md
CHANGED
|
@@ -17,7 +17,17 @@ We have designed the cli to be able to be run in headless mode, as such the foll
|
|
|
17
17
|
```
|
|
18
18
|
npm init routify [directory-name]
|
|
19
19
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-f, --force
|
|
20
|
+
-v, --version <version> use this to set the version of routify, e.g. 3
|
|
21
|
+
-t, --starter-template <starterTemplate> use this to set the starter template, e.g. starter-basic
|
|
22
|
+
-f, --force this option bypasses directory checks, be careful as might overwrite files!
|
|
23
|
+
-r, --force-refresh this option forces a refresh of the repos
|
|
24
|
+
-f, --features <features> optionally add features to your project, eg. "test", "prettier"
|
|
25
|
+
-s, --skip this option skips all prompts
|
|
26
|
+
-p, --package-manager <package-manager> this option sets the package manager to use, e.g. "npm", "pnpm" or "yarn"
|
|
27
|
+
-i, --install install dependencies after creating project
|
|
28
|
+
-d, --debug run in debug mode
|
|
29
|
+
-h, --help display help for command
|
|
23
30
|
```
|
|
31
|
+
|
|
32
|
+
### Contributors
|
|
33
|
+
See [CONTRIBUTORS.md](CONTRIBUTORS.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-routify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A powerful cli for super-powering your routify development experience",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,16 +32,14 @@
|
|
|
32
32
|
"url": "https://github.com/roxiness/create-routify/issues"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"
|
|
35
|
+
"@clack/prompts": "^0.6.3",
|
|
36
|
+
"commander": "^11.0.0",
|
|
37
|
+
"download-git-repo": "^3.0.2",
|
|
37
38
|
"log-symbols": "^5.1.0",
|
|
38
|
-
"
|
|
39
|
-
"prompts": "^2.4.2",
|
|
40
|
-
"simple-git": "^3.15.1",
|
|
39
|
+
"picocolors": "^1.0.0",
|
|
41
40
|
"update-notifier": "^6.0.2"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
44
|
-
"@types/prompts": "^2.4.2",
|
|
45
43
|
"@types/update-notifier": "^6.0.1"
|
|
46
44
|
}
|
|
47
45
|
}
|
package/src/bin.js
CHANGED
|
@@ -2,13 +2,47 @@
|
|
|
2
2
|
import updateNotifier from 'update-notifier';
|
|
3
3
|
import { readFile } from 'fs/promises';
|
|
4
4
|
import { run } from '../src/index.js';
|
|
5
|
-
import
|
|
5
|
+
import { program } from 'commander';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
run({ args });
|
|
7
|
+
parseArgs();
|
|
10
8
|
|
|
11
9
|
try {
|
|
12
10
|
const pkg = await readFile('../package.json', 'utf-8');
|
|
13
11
|
updateNotifier({ pkg: JSON.parse(pkg) }).notify();
|
|
14
12
|
} catch {}
|
|
13
|
+
|
|
14
|
+
function parseArgs(config) {
|
|
15
|
+
program
|
|
16
|
+
.argument('[dir]', 'name of the directory to create')
|
|
17
|
+
.option(
|
|
18
|
+
'-v, --version <version>',
|
|
19
|
+
'use this to set the version of routify, e.g. 3',
|
|
20
|
+
)
|
|
21
|
+
.option(
|
|
22
|
+
'-t, --starter-template <starterTemplate>',
|
|
23
|
+
'use this to set the starter template, e.g. starter-basic',
|
|
24
|
+
)
|
|
25
|
+
.option(
|
|
26
|
+
'-f, --force',
|
|
27
|
+
'this option bypasses directory checks, be careful as might overwrite files!',
|
|
28
|
+
)
|
|
29
|
+
.option(
|
|
30
|
+
'-r, --force-refresh',
|
|
31
|
+
'this option forces a refresh of the repos',
|
|
32
|
+
)
|
|
33
|
+
.option(
|
|
34
|
+
'--features <features...>',
|
|
35
|
+
'optionally add features to your project, eg. "test", "prettier"',
|
|
36
|
+
)
|
|
37
|
+
.option('-H, --headless', 'run in headless mode')
|
|
38
|
+
.option(
|
|
39
|
+
'-p, --package-manager <package-manager>',
|
|
40
|
+
'this option sets the package manager to use, e.g. "npm", "pnpm" or "yarn"',
|
|
41
|
+
)
|
|
42
|
+
.option('-i, --install', 'install dependencies after creating project')
|
|
43
|
+
.option('-d, --debug', 'run in debug mode')
|
|
44
|
+
.action((dir, options) => {
|
|
45
|
+
run({ dir, ...options });
|
|
46
|
+
})
|
|
47
|
+
.parse();
|
|
48
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,108 +1,330 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} Options
|
|
3
|
+
* @property {string} dir
|
|
4
|
+
* @property {string} projectDir
|
|
5
|
+
* @property {string} version
|
|
6
|
+
* @property {string} starterTemplate
|
|
7
|
+
* @property {Template} template
|
|
8
|
+
* @property {boolean} force
|
|
9
|
+
* @property {boolean} headless
|
|
10
|
+
* @property {boolean} debug
|
|
11
|
+
* @property {''|'npm'|'yarn'|'pnpm'} packageManager
|
|
12
|
+
* @property {string[]} features
|
|
13
|
+
*
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { mkdir, cp, rm } from 'fs/promises';
|
|
2
18
|
import { existsSync, readdirSync } from 'fs';
|
|
3
|
-
import {
|
|
19
|
+
import { join, relative, resolve } from 'path';
|
|
4
20
|
import symbols from 'log-symbols';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
const versions = {
|
|
11
|
-
2: () => import('./versions/two.js'),
|
|
12
|
-
3: () => import('./versions/three/index.js'),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const helpText = ` npm init routify [directory-name]
|
|
16
|
-
|
|
17
|
-
-h, --help get the help menu
|
|
18
|
-
-v, --version use this to set the version of routify, e.g. 3
|
|
19
|
-
-f, --force this option bypasses directory checks, be careful as might overwrite files!`;
|
|
20
|
-
|
|
21
|
-
async function getVersion(args) {
|
|
22
|
-
const argsVersion = args.v || args.version;
|
|
23
|
-
if (argsVersion) return argsVersion;
|
|
21
|
+
import color from 'picocolors';
|
|
22
|
+
import * as p from '@clack/prompts';
|
|
23
|
+
import { emitter, writePrettierConfig } from './utils/index.js';
|
|
24
|
+
import { addTests, removeTests } from './utils/patcher/test/index.js';
|
|
25
|
+
import { getTemplatesFromRepos } from './utils/repos.js';
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
name: 'version',
|
|
27
|
+
const prompts = {
|
|
28
|
+
version: () =>
|
|
29
|
+
p.select({
|
|
29
30
|
message: 'Routify Version:',
|
|
30
|
-
|
|
31
|
-
{
|
|
31
|
+
options: [
|
|
32
|
+
{ label: 'Routify 2', value: 2 },
|
|
32
33
|
{
|
|
33
|
-
|
|
34
|
+
label: `Routify 3`,
|
|
34
35
|
value: 3,
|
|
36
|
+
hint: 'This is a beta version',
|
|
35
37
|
},
|
|
36
38
|
],
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
39
|
+
initialValue: 3,
|
|
40
|
+
}),
|
|
41
|
+
dir: () =>
|
|
42
|
+
p.text({
|
|
43
|
+
message: 'Directory name:',
|
|
44
|
+
initialValue: '',
|
|
45
|
+
// hint: 'Leave empty to use current directory',
|
|
46
|
+
defaultValue: '.',
|
|
47
|
+
placeholder: 'Leave empty to use current directory',
|
|
48
|
+
}),
|
|
49
|
+
overwrite: () =>
|
|
50
|
+
p.confirm({
|
|
51
|
+
message: 'Directory is not empty, continue?',
|
|
52
|
+
initialValue: false,
|
|
53
|
+
}),
|
|
54
|
+
selectFeatures: (availableFeatures) =>
|
|
55
|
+
p.multiselect({
|
|
56
|
+
message: 'Select features:',
|
|
57
|
+
options: availableFeatures,
|
|
58
|
+
initialValues: availableFeatures
|
|
59
|
+
.filter((f) => f.initial)
|
|
60
|
+
.map((f) => f.value),
|
|
61
|
+
}),
|
|
62
|
+
selectTemplate: (templates) =>
|
|
63
|
+
p.select({
|
|
64
|
+
message: 'Select template:',
|
|
65
|
+
options: templates.map((template) => ({
|
|
66
|
+
label: template.manifest?.name || template.name,
|
|
67
|
+
value: template.name,
|
|
68
|
+
hint: template.manifest?.description || template.description,
|
|
69
|
+
})),
|
|
70
|
+
initialValue: 'starter-basic',
|
|
71
|
+
}),
|
|
72
|
+
selectPackageManager: () =>
|
|
73
|
+
p.select({
|
|
74
|
+
message: 'Install dependencies with:',
|
|
75
|
+
options: [
|
|
76
|
+
{ label: "Don't install", value: '' },
|
|
77
|
+
{ label: 'npm', value: 'npm' },
|
|
78
|
+
{ label: 'pnpm', value: 'pnpm' },
|
|
79
|
+
{ label: 'yarn', value: 'yarn' },
|
|
80
|
+
],
|
|
81
|
+
initialValue: '',
|
|
82
|
+
}),
|
|
83
|
+
nextSteps: (dir, packageManager) => {
|
|
84
|
+
const steps = [
|
|
85
|
+
dir === '.' ? '' : `cd ${dir}`,
|
|
86
|
+
packageManager ? '' : 'npm install',
|
|
87
|
+
`${packageManager || 'npm'} run dev`,
|
|
88
|
+
]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.map((step, i) => `${i + 1}. ${step}`)
|
|
91
|
+
.join('\n');
|
|
40
92
|
|
|
41
|
-
|
|
42
|
-
}
|
|
93
|
+
return `Next steps: \n${steps}`;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
43
96
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
97
|
+
const check = {
|
|
98
|
+
existingDir: async (options) => {
|
|
99
|
+
const proceed =
|
|
100
|
+
!existsSync(options.projectDir) ||
|
|
101
|
+
!readdirSync(options.projectDir).length ||
|
|
102
|
+
options.force ||
|
|
103
|
+
(await prompts.overwrite());
|
|
48
104
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
105
|
+
if (!proceed) {
|
|
106
|
+
p.cancel('Directory not empty');
|
|
107
|
+
process.exit();
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
version: (version) => {
|
|
111
|
+
if ([2, 3].includes(version.toString())) {
|
|
112
|
+
p.cancel(`Version ${version} not found`);
|
|
113
|
+
process.exit();
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getAvailableFeatures = (template) => {
|
|
119
|
+
const features = template.manifest.features || [];
|
|
120
|
+
if (template.manifest.test)
|
|
121
|
+
features.push({
|
|
122
|
+
label: 'test',
|
|
123
|
+
value: 'test',
|
|
124
|
+
hint: 'Add test files',
|
|
125
|
+
initial: true,
|
|
126
|
+
});
|
|
127
|
+
features.push({
|
|
128
|
+
label: 'prettier',
|
|
129
|
+
value: 'prettier',
|
|
130
|
+
hint: 'Add prettier config',
|
|
131
|
+
initial: true,
|
|
132
|
+
});
|
|
133
|
+
return features;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
*
|
|
138
|
+
* @param {} options
|
|
139
|
+
* @param {TemplateConfig} configs
|
|
140
|
+
*/
|
|
141
|
+
async function runPrompts(options, configs) {
|
|
142
|
+
console.clear();
|
|
143
|
+
p.intro(`${color.bgMagenta(color.black(' Routify CLI '))}`);
|
|
52
144
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
145
|
+
options.version = options.version || (await prompts.version());
|
|
146
|
+
check.version(options.version);
|
|
147
|
+
const config = configs.versions[options.version];
|
|
148
|
+
await setTemplates(configs, options);
|
|
149
|
+
options.dir = options.dir || (await prompts.dir());
|
|
150
|
+
|
|
151
|
+
options.projectDir = resolve(options.dir);
|
|
152
|
+
await check.existingDir(options);
|
|
153
|
+
|
|
154
|
+
while (!options.starterTemplate) {
|
|
155
|
+
const refreshOption = {
|
|
156
|
+
name: '[Refresh templates]',
|
|
157
|
+
hint: 'Update templates from remote',
|
|
158
|
+
};
|
|
159
|
+
const customTemplates = {
|
|
160
|
+
name: '[Include custom templates]',
|
|
161
|
+
hint: 'Include templates from 3rd party repos',
|
|
162
|
+
};
|
|
163
|
+
const templates = [...options.templates];
|
|
164
|
+
if (!options.forceRefresh) templates.push(refreshOption);
|
|
165
|
+
if (
|
|
166
|
+
!options.customTemplates &&
|
|
167
|
+
config.templatesRepos.find((repo) => !repo.includeByDefault)
|
|
168
|
+
)
|
|
169
|
+
templates.push(customTemplates);
|
|
170
|
+
options.starterTemplate =
|
|
171
|
+
options.starterTemplate ||
|
|
172
|
+
(await prompts.selectTemplate(templates));
|
|
173
|
+
|
|
174
|
+
if (options.starterTemplate === '[Refresh templates]') {
|
|
175
|
+
options.forceRefresh = true;
|
|
176
|
+
await setTemplates(configs, options);
|
|
177
|
+
options.starterTemplate = null;
|
|
178
|
+
}
|
|
179
|
+
if (options.starterTemplate === '[Include custom templates]') {
|
|
180
|
+
options.customTemplates = true;
|
|
181
|
+
await setTemplates(configs, options);
|
|
182
|
+
options.starterTemplate = null;
|
|
183
|
+
}
|
|
77
184
|
}
|
|
78
185
|
|
|
79
|
-
|
|
186
|
+
options.template = options.templates.find(
|
|
187
|
+
(t) => t.name === options.starterTemplate,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (!options.template)
|
|
191
|
+
p.cancel(`Template ${options.starterTemplate} not found`);
|
|
192
|
+
|
|
193
|
+
options.features =
|
|
194
|
+
options.features ||
|
|
195
|
+
(await prompts.selectFeatures(getAvailableFeatures(options.template)));
|
|
80
196
|
|
|
81
|
-
|
|
197
|
+
options.packageManager =
|
|
198
|
+
options.packageManager || (await prompts.selectPackageManager());
|
|
199
|
+
}
|
|
82
200
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
201
|
+
const copy = async (options) => {
|
|
202
|
+
const s = p.spinner();
|
|
203
|
+
s.start('Copying template to project directory');
|
|
204
|
+
await mkdir(options.projectDir, { recursive: true });
|
|
205
|
+
|
|
206
|
+
await cp(options.template.dir, options.projectDir, {
|
|
207
|
+
recursive: true,
|
|
208
|
+
});
|
|
209
|
+
if (existsSync(join(options.projectDir, 'manifest.js')))
|
|
210
|
+
await rm(join(options.projectDir, 'manifest.js'));
|
|
211
|
+
s.stop('Copied template to project directory');
|
|
212
|
+
};
|
|
87
213
|
|
|
88
|
-
|
|
214
|
+
const install = async (options) => {
|
|
215
|
+
if (options.packageManager) {
|
|
216
|
+
const s = p.spinner();
|
|
217
|
+
|
|
218
|
+
const { exec } = await import('child_process');
|
|
219
|
+
const { packageManager } = options;
|
|
220
|
+
const cwd = relative(process.cwd(), options.projectDir);
|
|
221
|
+
const cmd = `${packageManager} install`;
|
|
222
|
+
s.start(`Installing via ${packageManager}`);
|
|
223
|
+
await new Promise((resolve, reject) => {
|
|
224
|
+
exec(cmd, { cwd }, (err, stdout, stderr) => {
|
|
225
|
+
if (err) {
|
|
226
|
+
reject(err);
|
|
227
|
+
} else {
|
|
228
|
+
resolve(stdout);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
s.stop('Installed via pnpm');
|
|
234
|
+
}
|
|
235
|
+
};
|
|
89
236
|
|
|
90
|
-
|
|
91
|
-
|
|
237
|
+
/**
|
|
238
|
+
*
|
|
239
|
+
* @param {Options} options
|
|
240
|
+
*/
|
|
241
|
+
export const manageTests = async (options) => {
|
|
242
|
+
const { test } = options.template.manifest;
|
|
243
|
+
const dir = options.projectDir;
|
|
244
|
+
const shouldAddTest = options.features.includes('test');
|
|
245
|
+
if (shouldAddTest) {
|
|
246
|
+
await addTests(dir, test);
|
|
247
|
+
} else await removeTests(dir);
|
|
248
|
+
};
|
|
92
249
|
|
|
93
|
-
|
|
94
|
-
|
|
250
|
+
/**
|
|
251
|
+
* @param {Options} options
|
|
252
|
+
*/
|
|
253
|
+
const handleFeatures = async (options) => {
|
|
254
|
+
const s = p.spinner();
|
|
255
|
+
s.start('Set up features');
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
257
|
+
await manageTests(options);
|
|
258
|
+
if (options.features.includes('prettier')) {
|
|
259
|
+
writePrettierConfig(options.projectDir);
|
|
260
|
+
}
|
|
261
|
+
s.stop('Set up features');
|
|
262
|
+
};
|
|
95
263
|
|
|
96
|
-
|
|
264
|
+
/**
|
|
265
|
+
*
|
|
266
|
+
* @param {*} options
|
|
267
|
+
* @param {TemplateConfig} configs
|
|
268
|
+
*/
|
|
269
|
+
const normalizeOptions = async (options, configs) => {
|
|
270
|
+
options.version = options.version || 3;
|
|
271
|
+
const config = configs.versions[options.version];
|
|
272
|
+
await setTemplates(configs, options);
|
|
273
|
+
options.projectDir = resolve(options.dir);
|
|
274
|
+
options.starterTemplate = options.starterTemplate || config.defaultTemplate;
|
|
275
|
+
options.template = options.templates.find(
|
|
276
|
+
(t) => t.name === options.starterTemplate,
|
|
277
|
+
);
|
|
278
|
+
options.features = options.features || [];
|
|
279
|
+
check.version(options.version);
|
|
280
|
+
if (!options.template)
|
|
281
|
+
p.cancel(`Template ${options.starterTemplate} not found`);
|
|
282
|
+
};
|
|
97
283
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
284
|
+
/**
|
|
285
|
+
*
|
|
286
|
+
* @param {TemplateConfig} configs
|
|
287
|
+
* @param {*} options
|
|
288
|
+
*/
|
|
289
|
+
const setTemplates = async (configs, options) => {
|
|
290
|
+
const config = configs.versions[options.version];
|
|
291
|
+
options.templates = await getTemplatesFromRepos(
|
|
292
|
+
config.templatesRepos,
|
|
293
|
+
options.forceRefresh,
|
|
294
|
+
options.debug,
|
|
102
295
|
);
|
|
103
296
|
};
|
|
104
297
|
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
298
|
+
export const run = async (options) => {
|
|
299
|
+
const s = p.spinner();
|
|
300
|
+
emitter.on('download', (url) => s.start(`Downloading ${url}`));
|
|
301
|
+
emitter.on('downloaded', (url) => s.stop(`Downloaded ${url}`));
|
|
302
|
+
const configs = (await import('../config.js')).default;
|
|
303
|
+
|
|
304
|
+
const tools = { prompts: p };
|
|
305
|
+
// console.log(options);
|
|
306
|
+
// process.exit();
|
|
307
|
+
if (!options.headless) await runPrompts(options, configs);
|
|
308
|
+
else await normalizeOptions(options, configs);
|
|
309
|
+
|
|
310
|
+
await copy(options);
|
|
311
|
+
await handleFeatures(options);
|
|
312
|
+
const { preInstall, postInstall } = options.template.manifest;
|
|
313
|
+
if (preInstall) await preInstall(options, tools);
|
|
314
|
+
await install(options);
|
|
315
|
+
if (postInstall) await postInstall(options, tools);
|
|
316
|
+
|
|
317
|
+
p.note(
|
|
318
|
+
prompts.nextSteps(options.dir, options.packageManager) +
|
|
319
|
+
`\n\n${
|
|
320
|
+
symbols.success
|
|
321
|
+
} Need help? Join us on discord: ${color.underline(
|
|
322
|
+
color.bgMagenta('https://discord.com/invite/ntKJD5B'),
|
|
323
|
+
)}\n${
|
|
324
|
+
symbols.success
|
|
325
|
+
} Follow our twitter to get updates: ${color.underline(
|
|
326
|
+
color.bgMagenta('https://twitter.com/routifyjs'),
|
|
327
|
+
)}`,
|
|
328
|
+
);
|
|
329
|
+
p.outro('Happy coding!');
|
|
108
330
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { writeFile } from 'fs/promises';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import EventEmitter from 'events';
|
|
6
|
+
export const createDirname = (meta) => dirname(fileURLToPath(meta.url));
|
|
7
|
+
|
|
8
|
+
export const writePrettierConfig = async (dir) => {
|
|
9
|
+
const prettierConfigPath = join(dir, '.prettierrc');
|
|
10
|
+
if (!existsSync(prettierConfigPath)) {
|
|
11
|
+
await writeFile(
|
|
12
|
+
prettierConfigPath,
|
|
13
|
+
`{
|
|
14
|
+
"semi": false,
|
|
15
|
+
"singleQuote": true,
|
|
16
|
+
"trailingComma": "all",
|
|
17
|
+
"printWidth": 120
|
|
18
|
+
}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const emitter = new EventEmitter();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { rm } from 'fs/promises';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const resolveTestTemplate = (template) => {
|
|
6
|
+
if (typeof template === 'string') return template;
|
|
7
|
+
if (typeof template === 'object') {
|
|
8
|
+
const { page, contains } = template;
|
|
9
|
+
return `test('can see ${page}', async () => {
|
|
10
|
+
await router.url.push('${page}')
|
|
11
|
+
|
|
12
|
+
expect(document.body.innerHTML).toContain('${contains}')
|
|
13
|
+
})`;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} dir
|
|
19
|
+
* @param {TestTemplate[]} tests
|
|
20
|
+
*/
|
|
21
|
+
export const createTestFiles = async (dir, tests) => {
|
|
22
|
+
const testDir = join(dir, 'tests');
|
|
23
|
+
await mkdirSync(testDir, { recursive: true });
|
|
24
|
+
await writeFileSync(
|
|
25
|
+
join(testDir, 'test.spec.js'),
|
|
26
|
+
`/** @type { Router } */
|
|
27
|
+
let router
|
|
28
|
+
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
await import('../.routify/routify-init.js')
|
|
31
|
+
router = globalThis.__routify.routers[0]
|
|
32
|
+
await router.ready()
|
|
33
|
+
// wait for components to render
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
${tests.map(resolveTestTemplate).join('\n\n')}
|
|
38
|
+
`,
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const installVitest = async (dir) => {
|
|
43
|
+
// patch package.json
|
|
44
|
+
const packageJsonPath = join(dir, 'package.json');
|
|
45
|
+
const file = readFileSync(packageJsonPath, 'utf-8');
|
|
46
|
+
const packageJson = JSON.parse(file);
|
|
47
|
+
// if vitest isn't already installed, install it
|
|
48
|
+
if (!packageJson.devDependencies.vitest) {
|
|
49
|
+
packageJson.devDependencies.vitest = 'latest';
|
|
50
|
+
// add script
|
|
51
|
+
if (!packageJson.scripts.test) packageJson.scripts.test = 'vitest';
|
|
52
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
53
|
+
}
|
|
54
|
+
// add test entry to vite.config.js
|
|
55
|
+
const viteConfigPath = join(dir, 'vite.config.js');
|
|
56
|
+
const viteConfig = readFileSync(viteConfigPath, 'utf-8');
|
|
57
|
+
if (!viteConfig.includes('test:')) {
|
|
58
|
+
writeFileSync(
|
|
59
|
+
viteConfigPath,
|
|
60
|
+
viteConfig.replace(
|
|
61
|
+
'plugins: [',
|
|
62
|
+
`
|
|
63
|
+
test: {
|
|
64
|
+
environment: 'jsdom',
|
|
65
|
+
globals: true,
|
|
66
|
+
},
|
|
67
|
+
plugins: [`,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
// add global types to tsconfig.json
|
|
72
|
+
if (existsSync(join(dir, 'tsconfig.json'))) {
|
|
73
|
+
const tsConfigPath = join(dir, 'tsconfig.json');
|
|
74
|
+
const tsConfigFile = readFileSync(tsConfigPath, 'utf-8');
|
|
75
|
+
const tsConfig = JSON.parse(tsConfigFile);
|
|
76
|
+
tsConfig.compilerOptions.types = [
|
|
77
|
+
...(tsConfig.compilerOptions.types || []),
|
|
78
|
+
'vitest/globals',
|
|
79
|
+
];
|
|
80
|
+
tsConfig.include = [...(tsConfig.include || []), 'tests/**/*'];
|
|
81
|
+
writeFileSync(tsConfigPath, JSON.stringify(tsConfig, null, 2));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const removeTests = async (dir) => {
|
|
86
|
+
// remove tests folder (windows and unix)
|
|
87
|
+
const testDir = join(dir, 'tests');
|
|
88
|
+
await rm(testDir, { recursive: true, force: true });
|
|
89
|
+
// remove any line from scripts and devDependencies that contains test
|
|
90
|
+
const packageJsonPath = join(dir, 'package.json');
|
|
91
|
+
const file = readFileSync(packageJsonPath, 'utf-8');
|
|
92
|
+
const packageJson = JSON.parse(file);
|
|
93
|
+
packageJson.scripts = Object.fromEntries(
|
|
94
|
+
Object.entries(packageJson.scripts).filter(
|
|
95
|
+
([key]) => !key.includes('test'),
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
packageJson.devDependencies = Object.fromEntries(
|
|
99
|
+
Object.entries(packageJson.devDependencies).filter(
|
|
100
|
+
([key]) => !key.includes('test'),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
*
|
|
108
|
+
* @param {string} dir
|
|
109
|
+
* @param {TemplateManifest['test']} test
|
|
110
|
+
*/
|
|
111
|
+
export const addTests = async (dir, test) =>
|
|
112
|
+
Promise.all([createTestFiles(dir, test.tests), installVitest(dir)]);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readdir } from 'fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import download from 'download-git-repo';
|
|
7
|
+
import { emitter } from './index.js';
|
|
8
|
+
export const createDirname = (meta) => dirname(fileURLToPath(meta.url));
|
|
9
|
+
const __dirname = createDirname(import.meta);
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
const getReposPath = () => join(__dirname, '..', '..', '.repos');
|
|
14
|
+
|
|
15
|
+
const ensureRepo = async (url, force, onDownload) => {
|
|
16
|
+
// convert url to file safe name
|
|
17
|
+
const name = url.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
18
|
+
const repoPath = `${getReposPath()}/${name}`;
|
|
19
|
+
|
|
20
|
+
const localPath = url.match(/^local:(.*)/);
|
|
21
|
+
if (localPath) return join(__dirname, '..', '..', localPath[1]);
|
|
22
|
+
|
|
23
|
+
// if repo doesn't exist, download it
|
|
24
|
+
if (!existsSync(repoPath) || force) {
|
|
25
|
+
emitter.emit('download', url);
|
|
26
|
+
if (onDownload) onDownload();
|
|
27
|
+
await new Promise((resolve, reject) =>
|
|
28
|
+
download(url, repoPath, { clone: false }, (err) => {
|
|
29
|
+
if (err) reject(err);
|
|
30
|
+
else resolve(null);
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
emitter.emit('downloaded', url);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// return path to repo
|
|
37
|
+
return repoPath;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const getTemplatesDir = async (url, dir) => {
|
|
41
|
+
const repoPath = await ensureRepo(url);
|
|
42
|
+
|
|
43
|
+
return [repoPath, dir].filter(Boolean).join('/');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export class Manifest {
|
|
47
|
+
/**
|
|
48
|
+
* @param {Partial<TemplateManifest>} manifest
|
|
49
|
+
*/
|
|
50
|
+
constructor(manifest) {
|
|
51
|
+
this.name = manifest.name || '';
|
|
52
|
+
this.description = manifest.description || '';
|
|
53
|
+
this.test = manifest.test || null;
|
|
54
|
+
this.features = manifest.features || [];
|
|
55
|
+
this.postInstall = manifest.postInstall || (() => {});
|
|
56
|
+
this.preInstall = manifest.preInstall || (() => {});
|
|
57
|
+
this.error = manifest.error || null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const getManifestFromDir = async (dir) => {
|
|
62
|
+
try {
|
|
63
|
+
return await import(
|
|
64
|
+
pathToFileURL(join(dir, 'manifest.js')).pathname
|
|
65
|
+
).then((m) => new Manifest(m.default));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return new Manifest({ error });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
*
|
|
73
|
+
* @param {TemplateRepoConfig[]} repos
|
|
74
|
+
* @param {boolean} forceRefresh
|
|
75
|
+
* @param {boolean} debug
|
|
76
|
+
*/
|
|
77
|
+
export const getTemplatesFromRepos = async (repos, forceRefresh, debug) => {
|
|
78
|
+
/** @type {Template[]} */
|
|
79
|
+
const templates = [];
|
|
80
|
+
for (const repo of repos) {
|
|
81
|
+
const repoPath = await ensureRepo(repo.url, forceRefresh);
|
|
82
|
+
if (repo.templateType === 'single') {
|
|
83
|
+
templates.push({
|
|
84
|
+
dir: repoPath,
|
|
85
|
+
name: repo.name,
|
|
86
|
+
description: repo.description,
|
|
87
|
+
manifest: await getManifestFromDir(repoPath),
|
|
88
|
+
});
|
|
89
|
+
} else if (repo.templateType === 'directory') {
|
|
90
|
+
const templatePath = [repoPath, repo.path]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join('/');
|
|
93
|
+
const collectionTemplates = await getTemplatesFromDir(
|
|
94
|
+
templatePath,
|
|
95
|
+
debug,
|
|
96
|
+
);
|
|
97
|
+
templates.push(...collectionTemplates);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return templates;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the directory of @roxi/routify/examples
|
|
105
|
+
*/
|
|
106
|
+
export const getRoutifyExamplesDir = () => {
|
|
107
|
+
const routifyPkgJsonPath = require.resolve('@roxi/routify/package.json');
|
|
108
|
+
return resolve(routifyPkgJsonPath, '..', 'examples');
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns every template with a manifest.js file
|
|
113
|
+
* @param {string} routifyExamplesDir
|
|
114
|
+
* @param {boolean=} debug
|
|
115
|
+
* @returns {Promise<Template[]>}
|
|
116
|
+
*/
|
|
117
|
+
export const getTemplatesFromDir = async (routifyExamplesDir, debug) => {
|
|
118
|
+
let dirNames = await readdir(routifyExamplesDir);
|
|
119
|
+
return Promise.all(
|
|
120
|
+
dirNames
|
|
121
|
+
.map((name) => ({ name, dir: join(routifyExamplesDir, name) }))
|
|
122
|
+
.filter(({ dir }) => existsSync(join(dir, 'manifest.js')))
|
|
123
|
+
.map(async ({ dir, name }) => {
|
|
124
|
+
return {
|
|
125
|
+
dir,
|
|
126
|
+
name,
|
|
127
|
+
manifest: await getManifestFromDir(dir),
|
|
128
|
+
};
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getTemplate, onCancel } from '../../utils/prompts.js';
|
|
2
|
+
import { readdir, cp, rm } from 'fs/promises';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
|
+
import prompts from 'prompts';
|
|
6
|
+
import k from 'kleur';
|
|
7
|
+
import { getRoutifyExamplesDir, routifyIntro } from './utils.js';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { addTests } from '../../utils/patcher/test/index.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
export const run = async (options) => {
|
|
14
|
+
const { dir, force, version, starter } = options;
|
|
15
|
+
console.log('options', options);
|
|
16
|
+
routifyIntro();
|
|
17
|
+
|
|
18
|
+
const routifyExamplesDir = getRoutifyExamplesDir();
|
|
19
|
+
const project = await getTemplate(routifyExamplesDir, starter);
|
|
20
|
+
console.log('project', project);
|
|
21
|
+
if (project.test) {
|
|
22
|
+
const { tests } = project.test;
|
|
23
|
+
console.log(tests);
|
|
24
|
+
addTests(projectDir, project.test);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// await cp(exampleDir, projectDir, { recursive: true });
|
|
28
|
+
// await rm(join(projectDir, 'manifest.js'));
|
|
29
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import k from 'kleur';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
export function routifyIntro() {
|
|
7
|
+
console.log();
|
|
8
|
+
console.log(
|
|
9
|
+
` ${k.underline(
|
|
10
|
+
`${k.bold().magenta('Routify')} ${k.bold('3')} beta`,
|
|
11
|
+
)}`,
|
|
12
|
+
);
|
|
13
|
+
console.log(
|
|
14
|
+
` - Follow our twitter to get updates: ${k.blue(
|
|
15
|
+
'https://twitter.com/routifyjs',
|
|
16
|
+
)}`,
|
|
17
|
+
);
|
|
18
|
+
console.log(
|
|
19
|
+
` - Or join our discord: ${k.blue(
|
|
20
|
+
'https://discord.com/invite/ntKJD5B',
|
|
21
|
+
)}`,
|
|
22
|
+
);
|
|
23
|
+
console.log();
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import logSymbols from 'log-symbols';
|
|
2
|
+
import simpleGit from 'simple-git';
|
|
3
|
+
import { rmSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import k from 'kleur';
|
|
6
|
+
|
|
7
|
+
export const run = async ({ projectDir, args }) => {
|
|
8
|
+
const git = simpleGit(projectDir);
|
|
9
|
+
|
|
10
|
+
console.log(k.blue(`\n${logSymbols.info} Cloning template...`));
|
|
11
|
+
|
|
12
|
+
await git.clone('https://github.com/roxiness/routify-starter', projectDir);
|
|
13
|
+
|
|
14
|
+
rmSync(join(projectDir, '.git'), {
|
|
15
|
+
recursive: true,
|
|
16
|
+
force: true,
|
|
17
|
+
});
|
|
18
|
+
};
|
package/src/utils/prompts.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { onCancel } from '../../utils/prompts.js';
|
|
2
|
-
import { readdir, cp, rm } from 'fs/promises';
|
|
3
|
-
import { join, dirname } from 'path';
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
|
-
import prompts from 'prompts';
|
|
6
|
-
import k from 'kleur';
|
|
7
|
-
import { getRoutifyExamplesDir } from './utils.js';
|
|
8
|
-
import { existsSync } from 'fs';
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
|
-
function text() {
|
|
13
|
-
console.log();
|
|
14
|
-
console.log(
|
|
15
|
-
k.red(` ! R3 is under heavy work, expect bugs and missing features`),
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
console.log();
|
|
19
|
-
console.log(
|
|
20
|
-
` ${k.underline(`${k.bold().magenta('Routify')} ${k.bold('3')}`)}`,
|
|
21
|
-
);
|
|
22
|
-
console.log(
|
|
23
|
-
` - Follow our twitter to get updates: ${k.blue(
|
|
24
|
-
'https://twitter.com/routifyjs',
|
|
25
|
-
)}`,
|
|
26
|
-
);
|
|
27
|
-
console.log(
|
|
28
|
-
` - Or join our discord: ${k.blue(
|
|
29
|
-
'https://discord.com/invite/ntKJD5B',
|
|
30
|
-
)}`,
|
|
31
|
-
);
|
|
32
|
-
console.log();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function getExampleDir() {
|
|
36
|
-
const routifyExamplesDir = getRoutifyExamplesDir();
|
|
37
|
-
let dirNames = await readdir(routifyExamplesDir);
|
|
38
|
-
const projects = await Promise.all(
|
|
39
|
-
dirNames
|
|
40
|
-
.map((name) => ({ name, dir: join(routifyExamplesDir, name) }))
|
|
41
|
-
.filter(({ dir }) => existsSync(join(dir, 'manifest.js')))
|
|
42
|
-
.map(async ({ dir, name }) => {
|
|
43
|
-
try {
|
|
44
|
-
return await import(
|
|
45
|
-
pathToFileURL(join(dir, 'manifest.js')).pathname
|
|
46
|
-
).then((m) => ({ dir, name, manifest: m.default }));
|
|
47
|
-
} catch (err) {
|
|
48
|
-
return {
|
|
49
|
-
dir,
|
|
50
|
-
name,
|
|
51
|
-
manifest: {
|
|
52
|
-
name,
|
|
53
|
-
description: 'Could not read template info',
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}),
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const { project } = await prompts(
|
|
61
|
-
{
|
|
62
|
-
message: 'Please select a starter template',
|
|
63
|
-
name: 'project',
|
|
64
|
-
type: 'select',
|
|
65
|
-
choices: projects.filter(Boolean).map((value) => ({
|
|
66
|
-
title: value.manifest.name,
|
|
67
|
-
description: value.manifest.description,
|
|
68
|
-
value,
|
|
69
|
-
})),
|
|
70
|
-
},
|
|
71
|
-
{ onCancel },
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
return project.dir;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const run = async ({ projectDir }) => {
|
|
78
|
-
text();
|
|
79
|
-
|
|
80
|
-
const exampleDir = await getExampleDir();
|
|
81
|
-
|
|
82
|
-
await cp(exampleDir, projectDir, { recursive: true });
|
|
83
|
-
await rm(join(projectDir, 'manifest.js'));
|
|
84
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
const require = createRequire(import.meta.url);
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Returns the directory of @roxi/routify/examples
|
|
7
|
-
*/
|
|
8
|
-
export const getRoutifyExamplesDir = () => {
|
|
9
|
-
const routifyPkgJsonPath = require.resolve('@roxi/routify/package.json');
|
|
10
|
-
return resolve(routifyPkgJsonPath, '..', 'examples');
|
|
11
|
-
};
|