@quilted/create 0.1.68 → 0.1.70

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/source/cli.ts CHANGED
@@ -5,7 +5,7 @@ import {AbortError, stripIndent, color, parseArguments} from '@quilted/cli-kit';
5
5
  import {printHelp} from './help.ts';
6
6
  import {prompt} from './shared.ts';
7
7
 
8
- const VALID_PROJECT_KINDS = new Set(['app', 'package', 'module']);
8
+ const VALID_PROJECT_KINDS = new Set(['app', 'package', 'module', 'service']);
9
9
 
10
10
  run().catch((error) => {
11
11
  if (AbortError.test(error)) return;
@@ -59,6 +59,7 @@ async function run() {
59
59
  {title: 'App', value: 'app'},
60
60
  {title: 'Module', value: 'module'},
61
61
  {title: 'Package', value: 'package'},
62
+ {title: 'Backend service', value: 'service'},
62
63
  ],
63
64
  });
64
65
  }
@@ -79,5 +80,10 @@ async function run() {
79
80
  await createProject();
80
81
  break;
81
82
  }
83
+ case 'service': {
84
+ const {createService} = await import('./service.ts');
85
+ await createService();
86
+ break;
87
+ }
82
88
  }
83
89
  }
package/source/help.ts CHANGED
@@ -5,7 +5,7 @@ export function printHelp({
5
5
  options: customOptions,
6
6
  packageManager,
7
7
  }: {
8
- kind?: 'app' | 'package' | 'module';
8
+ kind?: 'app' | 'package' | 'module' | 'service';
9
9
  options?: string;
10
10
  packageManager?: string;
11
11
  } = {}) {
package/source/module.ts CHANGED
@@ -60,7 +60,7 @@ export async function createModule() {
60
60
  const partOfMonorepo = inWorkspace || createAsMonorepo;
61
61
 
62
62
  const moduleDirectory = createAsMonorepo
63
- ? path.join(directory, 'app')
63
+ ? path.join(directory, toValidPackageName(name))
64
64
  : directory;
65
65
 
66
66
  if (fs.existsSync(directory)) {
@@ -335,10 +335,16 @@ async function getEntry(argv: Arguments, {name}: {name: string}) {
335
335
  return argv['--entry'];
336
336
  }
337
337
 
338
+ const defaultEntry = `${toValidPackageName(name)}.ts`;
339
+
340
+ if (argv['--yes']) {
341
+ return defaultEntry;
342
+ }
343
+
338
344
  const entry = await prompt({
339
345
  type: 'text',
340
346
  message: 'What do you want to name your entry file?',
341
- initial: `${toValidPackageName(name)}.ts`,
347
+ initial: defaultEntry,
342
348
  });
343
349
 
344
350
  return entry;
@@ -0,0 +1,396 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ import arg from 'arg';
5
+ import * as color from 'colorette';
6
+ import {stripIndent} from 'common-tags';
7
+
8
+ import {printHelp} from './help.ts';
9
+ import {
10
+ format,
11
+ loadTemplate,
12
+ createOutputTarget,
13
+ isEmpty,
14
+ emptyDirectory,
15
+ toValidPackageName,
16
+ relativeDirectoryForDisplay,
17
+ mergeWorkspaceAndProjectPackageJsons,
18
+ } from './shared.ts';
19
+ import {
20
+ prompt,
21
+ getInWorkspace,
22
+ getCreateAsMonorepo,
23
+ getExtrasToSetup,
24
+ getPackageManager,
25
+ getShouldInstall,
26
+ } from './shared/prompts.ts';
27
+ import {addToTsConfig} from './shared/tsconfig.ts';
28
+ import {addToPackageManagerWorkspaces} from './shared/package-manager.ts';
29
+
30
+ type Arguments = ReturnType<typeof getArgv>;
31
+
32
+ export async function createService() {
33
+ const argv = getArgv();
34
+
35
+ if (argv['--help']) {
36
+ printHelp({
37
+ kind: 'service',
38
+ packageManager: argv['--package-manager']?.toLowerCase(),
39
+ });
40
+ return;
41
+ }
42
+
43
+ const inWorkspace = await getInWorkspace(argv);
44
+ const name = await getName(argv);
45
+ const directory = await getDirectory(argv, {name});
46
+ const entry = await getEntry(argv, {name});
47
+
48
+ const createAsMonorepo =
49
+ !inWorkspace &&
50
+ (await getCreateAsMonorepo(argv, {
51
+ type: 'service',
52
+ default: false,
53
+ }));
54
+ const setupExtras = await getExtrasToSetup(argv, {inWorkspace});
55
+ const shouldInstall = await getShouldInstall(argv);
56
+ const packageManager = await getPackageManager(argv, {root: directory});
57
+
58
+ const partOfMonorepo = inWorkspace || createAsMonorepo;
59
+
60
+ const serviceDirectory = createAsMonorepo
61
+ ? path.join(directory, toValidPackageName(name))
62
+ : directory;
63
+
64
+ if (fs.existsSync(directory)) {
65
+ await emptyDirectory(directory);
66
+
67
+ if (serviceDirectory !== directory) {
68
+ fs.mkdirSync(serviceDirectory, {recursive: true});
69
+ }
70
+ } else {
71
+ fs.mkdirSync(serviceDirectory, {recursive: true});
72
+ }
73
+
74
+ const rootDirectory = inWorkspace ? process.cwd() : directory;
75
+ const outputRoot = createOutputTarget(rootDirectory);
76
+ const serviceTemplate = loadTemplate('service-basic');
77
+ const workspaceTemplate = loadTemplate('workspace');
78
+
79
+ // If we aren’t already in a workspace, copy the workspace files over, which
80
+ // are needed if we are making a monorepo or not.
81
+ if (!inWorkspace) {
82
+ await workspaceTemplate.copy(directory, (file) => {
83
+ // When this is a single project, we use the project’s Quilt configuration as the base.
84
+ if (file === 'quilt.workspace.ts') return createAsMonorepo;
85
+
86
+ // We need to make some adjustments to the root package.json
87
+ if (file === 'package.json') return false;
88
+
89
+ return true;
90
+ });
91
+
92
+ // If we are creating a monorepo, we need to add the root package.json and
93
+ // package manager workspace configuration.
94
+ if (createAsMonorepo) {
95
+ const serviceRelativeToRoot = relativeDirectoryForDisplay(
96
+ path.relative(directory, serviceDirectory),
97
+ );
98
+
99
+ const workspacePackageJson = JSON.parse(
100
+ await workspaceTemplate.read('package.json'),
101
+ );
102
+
103
+ workspacePackageJson.name = toValidPackageName(name!);
104
+ workspacePackageJson.workspaces = [serviceRelativeToRoot, './packages/*'];
105
+
106
+ if (packageManager.type === 'pnpm') {
107
+ await outputRoot.write(
108
+ 'pnpm-workspace.yaml',
109
+ await format(
110
+ `
111
+ packages:
112
+ - '${serviceRelativeToRoot}'
113
+ - './packages/*'
114
+ `,
115
+ {as: 'yaml'},
116
+ ),
117
+ );
118
+ }
119
+
120
+ await outputRoot.write(
121
+ 'package.json',
122
+ await format(JSON.stringify(workspacePackageJson), {
123
+ as: 'json-stringify',
124
+ }),
125
+ );
126
+ } else {
127
+ const [projectPackageJson, projectTSConfig, workspacePackageJson] =
128
+ await Promise.all([
129
+ serviceTemplate
130
+ .read('package.json')
131
+ .then((content) => JSON.parse(content)),
132
+ serviceTemplate
133
+ .read('tsconfig.json')
134
+ .then((content) => JSON.parse(content)),
135
+ workspaceTemplate
136
+ .read('package.json')
137
+ .then((content) => JSON.parse(content)),
138
+ ]);
139
+
140
+ const combinedPackageJson = mergeWorkspaceAndProjectPackageJsons(
141
+ projectPackageJson,
142
+ workspacePackageJson,
143
+ );
144
+
145
+ adjustPackageJson(combinedPackageJson, {name, entry});
146
+ delete combinedPackageJson.workspaces;
147
+
148
+ let quiltProject = await serviceTemplate.read('quilt.project.ts');
149
+ quiltProject = quiltProject
150
+ .replace('quiltService', 'quiltWorkspace, quiltService')
151
+ .replace('quiltService(', 'quiltWorkspace(), quiltService(')
152
+ .replace('service.ts', entry.replace(/^\.[/]/, ''));
153
+
154
+ await outputRoot.write(
155
+ 'quilt.project.ts',
156
+ await format(quiltProject, {as: 'typescript'}),
157
+ );
158
+
159
+ await outputRoot.write(
160
+ 'package.json',
161
+ await format(JSON.stringify(combinedPackageJson), {
162
+ as: 'json-stringify',
163
+ }),
164
+ );
165
+
166
+ await outputRoot.write(
167
+ 'tsconfig.json',
168
+ await format(JSON.stringify(projectTSConfig), {as: 'json'}),
169
+ );
170
+ }
171
+
172
+ if (setupExtras.has('github')) {
173
+ await loadTemplate('github').copy(directory);
174
+ }
175
+
176
+ if (setupExtras.has('vscode')) {
177
+ await loadTemplate('vscode').copy(directory);
178
+ }
179
+ }
180
+
181
+ await serviceTemplate.copy(serviceDirectory, (file) => {
182
+ // If we are in a monorepo, we can use all the template files as they are
183
+ if (file === 'tsconfig.json') {
184
+ return partOfMonorepo;
185
+ }
186
+
187
+ // We will adjust the entry file and quilt project file
188
+ if (file === 'service.ts' || file === 'quilt.project.ts') {
189
+ return false;
190
+ }
191
+
192
+ // We need to make some adjustments the project’s package.json
193
+ return file !== 'package.json';
194
+ });
195
+
196
+ await outputRoot.write(
197
+ path.join(serviceDirectory, entry),
198
+ await serviceTemplate.read('service.ts'),
199
+ );
200
+
201
+ let quiltProject = await serviceTemplate.read('quilt.project.ts');
202
+ quiltProject = quiltProject.replace(
203
+ 'service.ts',
204
+ entry.replace(/^\.[/]/, ''),
205
+ );
206
+
207
+ await outputRoot.write(
208
+ path.join(serviceDirectory, 'quilt.project.ts'),
209
+ await format(quiltProject, {as: 'typescript'}),
210
+ );
211
+
212
+ if (partOfMonorepo) {
213
+ // Write the app’s package.json (the root one was already created)
214
+ const projectPackageJson = JSON.parse(
215
+ await serviceTemplate.read('package.json'),
216
+ );
217
+
218
+ adjustPackageJson(projectPackageJson, {name, entry});
219
+
220
+ await outputRoot.write(
221
+ path.join(serviceDirectory, 'package.json'),
222
+ await format(JSON.stringify(projectPackageJson), {
223
+ as: 'json-stringify',
224
+ }),
225
+ );
226
+
227
+ await Promise.all([
228
+ addToTsConfig(serviceDirectory, outputRoot),
229
+ addToPackageManagerWorkspaces(
230
+ serviceDirectory,
231
+ outputRoot,
232
+ packageManager.type,
233
+ ),
234
+ ]);
235
+ }
236
+
237
+ if (shouldInstall) {
238
+ console.log();
239
+ // TODO: better loading, handle errors
240
+ await packageManager.install();
241
+ }
242
+
243
+ const commands: string[] = [];
244
+
245
+ if (!inWorkspace && directory !== process.cwd()) {
246
+ commands.push(
247
+ `cd ${color.cyan(
248
+ relativeDirectoryForDisplay(path.relative(process.cwd(), directory)),
249
+ )} ${color.dim('# Move into your new service’s directory')}`,
250
+ );
251
+ }
252
+
253
+ if (!shouldInstall) {
254
+ commands.push(
255
+ `${packageManager.commands.install()} ${color.dim(
256
+ '# Install all your dependencies',
257
+ )}`,
258
+ );
259
+ }
260
+
261
+ if (commands.length === 0) {
262
+ console.log();
263
+ console.log('Your new service is ready to go!');
264
+ } else {
265
+ const whatsNext = stripIndent`
266
+ Your new service is ready to go! There’s just ${
267
+ commands.length > 1 ? 'a few more steps' : 'one more step'
268
+ } you’ll need to take
269
+ in order to start developing:
270
+ `;
271
+
272
+ console.log();
273
+ console.log(whatsNext);
274
+ console.log();
275
+ console.log(commands.map((command) => ` ${command}`).join('\n'));
276
+ }
277
+
278
+ const followUp = stripIndent`
279
+ Quilt can also help you build, develop, test, lint, and type-check your new service.
280
+ You can learn more about building services with Quilt by reading the documentation:
281
+ ${color.underline(
282
+ color.magenta(
283
+ 'https://github.com/lemonmade/quilt/tree/main/documentation',
284
+ ),
285
+ )}
286
+
287
+ Have fun! 🎉
288
+ `;
289
+
290
+ console.log();
291
+ console.log(followUp);
292
+ }
293
+
294
+ // Argument handling
295
+
296
+ function getArgv() {
297
+ const argv = arg(
298
+ {
299
+ '--yes': Boolean,
300
+ '-y': '--yes',
301
+ '--name': String,
302
+ '--directory': String,
303
+ '--entry': String,
304
+ '--install': Boolean,
305
+ '--no-install': Boolean,
306
+ '--monorepo': Boolean,
307
+ '--no-monorepo': Boolean,
308
+ '--package-manager': String,
309
+ '--extras': [String],
310
+ '--no-extras': Boolean,
311
+ '--help': Boolean,
312
+ '-h': '--help',
313
+ },
314
+ {permissive: true},
315
+ );
316
+
317
+ return argv;
318
+ }
319
+
320
+ async function getName(argv: Arguments) {
321
+ let {'--name': name} = argv;
322
+
323
+ if (name == null) {
324
+ name = await prompt({
325
+ type: 'text',
326
+ message: 'What would you like to name your new service?',
327
+ initial: 'my-service',
328
+ });
329
+ }
330
+
331
+ return name!;
332
+ }
333
+
334
+ async function getEntry(argv: Arguments, {name}: {name: string}) {
335
+ if (argv['--entry']) {
336
+ return argv['--entry'];
337
+ }
338
+
339
+ const defaultEntry = `${toValidPackageName(name)}.ts`;
340
+
341
+ const entry = await prompt({
342
+ type: 'text',
343
+ message: 'What do you want to name your entry file?',
344
+ initial: defaultEntry,
345
+ });
346
+
347
+ return entry;
348
+ }
349
+
350
+ async function getDirectory(argv: Arguments, {name}: {name: string}) {
351
+ let directory = path.resolve(
352
+ argv['--directory'] ?? toValidPackageName(name!),
353
+ );
354
+
355
+ while (!argv['--yes']) {
356
+ if (fs.existsSync(directory) && !(await isEmpty(directory))) {
357
+ const relativeDirectory = path.relative(process.cwd(), directory);
358
+
359
+ const empty = await prompt({
360
+ type: 'confirm',
361
+ message: `Directory ${color.bold(
362
+ relativeDirectoryForDisplay(relativeDirectory),
363
+ )} is not empty, is it safe to empty it?`,
364
+ initial: true,
365
+ });
366
+
367
+ if (empty) break;
368
+
369
+ const promptDirectory = await prompt({
370
+ type: 'text',
371
+ message: 'What directory do you want to create your new service in?',
372
+ });
373
+
374
+ directory = path.resolve(promptDirectory);
375
+ } else {
376
+ break;
377
+ }
378
+ }
379
+
380
+ return directory;
381
+ }
382
+
383
+ function adjustPackageJson(
384
+ packageJson: Record<string, any>,
385
+ {
386
+ name,
387
+ }: {
388
+ name: string;
389
+ entry: string;
390
+ },
391
+ ) {
392
+ packageJson.name = name;
393
+ packageJson.main = `./build/runtime/runtime.js`;
394
+
395
+ return packageJson;
396
+ }
@@ -35,7 +35,7 @@ export async function getCreateAsMonorepo(
35
35
  {
36
36
  type,
37
37
  default: defaultCreateAsMonorepo = true,
38
- }: {type: 'app' | 'package' | 'module'; default?: boolean},
38
+ }: {type: 'app' | 'package' | 'module' | 'service'; default?: boolean},
39
39
  ) {
40
40
  let createAsMonorepo: boolean;
41
41
 
package/source/shared.ts CHANGED
@@ -13,6 +13,7 @@ export function loadTemplate(
13
13
  | 'app-graphql'
14
14
  | 'app-trpc'
15
15
  | 'module'
16
+ | 'service-basic'
16
17
  | 'workspace'
17
18
  | 'github'
18
19
  | 'vscode',
@@ -84,6 +85,7 @@ async function templateDirectory(
84
85
  | 'app-graphql'
85
86
  | 'app-trpc'
86
87
  | 'module'
88
+ | 'service-basic'
87
89
  | 'workspace'
88
90
  | 'github'
89
91
  | 'vscode',
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "template-service-basic",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "devDependencies": {
7
+ "@quilted/quilt": "^0.5.0"
8
+ },
9
+ "eslintConfig": {
10
+ "extends": "@quilted/eslint-config/project"
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ import {createProject, quiltService} from '@quilted/craft';
2
+
3
+ export default createProject((project) => {
4
+ project.use(
5
+ quiltService({
6
+ entry: './service.ts',
7
+ }),
8
+ );
9
+ });
@@ -0,0 +1,7 @@
1
+ import {createRequestRouter} from '@quilted/quilt/request-router';
2
+
3
+ const app = createRequestRouter();
4
+
5
+ app.get('/', () => new Response('Hello, world!'));
6
+
7
+ export default app;
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@quilted/typescript/project.json",
3
+ "compilerOptions": {
4
+ "outDir": "build/typescript"
5
+ },
6
+ "include": ["**/*"],
7
+ "exclude": ["build"],
8
+ "references": []
9
+ }