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 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
- -h, --help get the help menu
21
- -v, --version use this to set the version of routify, e.g. 3
22
- -f, --force this option bypasses directory checks, be careful as might overwrite files!
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.4.2",
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
- "@roxi/routify": "^3.0.0-next.172",
36
- "kleur": "^4.1.5",
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
- "minimist": "^1.2.7",
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 minimist from 'minimist';
5
+ import { program } from 'commander';
6
6
 
7
- const args = minimist(process.argv.slice(2));
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
- import { onCancel } from './utils/prompts.js';
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 { mkdir } from 'fs/promises';
19
+ import { join, relative, resolve } from 'path';
4
20
  import symbols from 'log-symbols';
5
- import { relative } from 'path';
6
- import { resolve } from 'path';
7
- import prompts from 'prompts';
8
- import k from 'kleur';
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
- const { version } = await prompts(
26
- {
27
- type: 'select',
28
- name: 'version',
27
+ const prompts = {
28
+ version: () =>
29
+ p.select({
29
30
  message: 'Routify Version:',
30
- choices: [
31
- { title: 'Routify 2', value: 2 },
31
+ options: [
32
+ { label: 'Routify 2', value: 2 },
32
33
  {
33
- title: `Routify 3 ${k.bold().magenta('[BETA]')}`,
34
+ label: `Routify 3`,
34
35
  value: 3,
36
+ hint: 'This is a beta version',
35
37
  },
36
38
  ],
37
- },
38
- { onCancel },
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
- return version;
42
- }
93
+ return `Next steps: \n${steps}`;
94
+ },
95
+ };
43
96
 
44
- export const run = async ({ args }) => {
45
- console.log(` ${k.dim(`v${'1.0.0'}`)}`);
46
- console.log(` ${k.bold().magenta('Routify')} ${k.magenta().dim('CLI')}`);
47
- console.log();
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
- if (args.h || args.help) {
50
- return console.log(helpText);
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
- const version = await getVersion(args);
54
- const force = args.f || args.force;
55
-
56
- if (!Object.keys(versions).includes(version.toString()))
57
- return console.log(` ${k.red(`Version ${version} not found`)}`);
58
-
59
- const projectName = args._[0] || '.';
60
- const projectDir = resolve(projectName.toString());
61
-
62
- if (
63
- existsSync(projectDir) &&
64
- readdirSync(projectDir).length > 0 &&
65
- !force
66
- ) {
67
- const { proceed } = await prompts(
68
- {
69
- type: 'confirm',
70
- message: `Directory is not empty, continue?`,
71
- name: 'proceed',
72
- },
73
- { onCancel },
74
- );
75
-
76
- if (!proceed) return onCancel();
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
- await mkdir(projectDir, { recursive: true });
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
- await runVersion(version, { args, projectDir });
197
+ options.packageManager =
198
+ options.packageManager || (await prompts.selectPackageManager());
199
+ }
82
200
 
83
- console.log();
84
- console.log(` ${k.green('All Done!')}`);
85
- console.log();
86
- console.log(` Now you can:`);
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
- let i = 1;
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
- if (relative(process.cwd(), projectDir) != '')
91
- console.log(` ${i++}) cd ${relative(process.cwd(), projectDir)}`);
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
- console.log(` ${i++}) npm install`);
94
- console.log(` ${i++}) npm run dev`);
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
- console.log();
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
- console.log(
99
- `${symbols.success} If you need help, ${k.blue(
100
- 'join the Discord',
101
- )}: https://discord.com/invite/ntKJD5B`,
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 runVersion = async (version, args) => {
106
- const { run } = await versions[version]();
107
- return run(args);
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
+ };
@@ -1,10 +0,0 @@
1
- import k from 'kleur';
2
-
3
- export const onCancel = () => {
4
- console.log();
5
- console.log(
6
- ` ${k.bold().red('Exited')} ${k.magenta().dim('create-routify')}`,
7
- );
8
-
9
- process.exit(0);
10
- };
@@ -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
- };