@quilted/create 0.1.35 → 0.1.37

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/package.ts CHANGED
@@ -8,13 +8,14 @@ import {
8
8
  format,
9
9
  loadTemplate,
10
10
  createOutputTarget,
11
- mergeDependencies,
12
11
  isEmpty,
13
12
  emptyDirectory,
14
13
  toValidPackageName,
15
14
  relativeDirectoryForDisplay,
15
+ mergeWorkspaceAndProjectPackageJsons,
16
16
  } from './shared';
17
17
  import {
18
+ getInWorkspace,
18
19
  getCreateAsMonorepo,
19
20
  getExtrasToSetup,
20
21
  getPackageManager,
@@ -30,6 +31,13 @@ export async function createProject() {
30
31
 
31
32
  if (args['--help']) {
32
33
  const additionalOptions = stripIndent`
34
+ ${color.cyan(`--description`)}, ${color.cyan(`--no-description`)}
35
+ A short description of the package. If you don’t provide this option, the command will ask
36
+ you for a description later.
37
+ ${color.dim(
38
+ `@see https://docs.npmjs.com/cli/v9/configuring-npm/package-json#description`,
39
+ )}
40
+
33
41
  ${color.cyan(`--react`)}, ${color.cyan(`--no-react`)}
34
42
  Whether this package will use React. If you don’t provide this option, the command
35
43
  will ask you about it later.
@@ -38,6 +46,13 @@ export async function createProject() {
38
46
  Whether this package will be published for other projects to install. If you do not
39
47
  provide this option, the command will ask you about it later.
40
48
 
49
+ ${color.cyan(`--repository`)}, ${color.cyan(`--no-repository`)}
50
+ The URL of a git repository where your code lives. If you do not provide this option,
51
+ this command will try to guess the correct repository to use based on existing packages.
52
+ ${color.dim(
53
+ `@see https://docs.npmjs.com/cli/v9/configuring-npm/package-json#repository`,
54
+ )}
55
+
41
56
  ${color.cyan(`--registry`)}
42
57
  The package registry to publish this package to. This option only applies if you create
43
58
  a public package. If you do not provide this option, it will use the default NPM registry.
@@ -51,17 +66,19 @@ export async function createProject() {
51
66
  return;
52
67
  }
53
68
 
54
- const inWorkspace = fs.existsSync('quilt.workspace.ts');
55
-
56
69
  const name = await getName(args);
70
+ const description = await getDescription(args);
71
+ const inWorkspace = await getInWorkspace(args);
57
72
  const directory = await getDirectory(args, {name, inWorkspace});
58
73
  const isPublic = await getPublic(args);
74
+ const repository = await getRepository(args, {inWorkspace});
59
75
  const useReact = await getReact(args);
60
76
 
61
- const createAsMonorepo = !inWorkspace && (await getCreateAsMonorepo(args));
62
- const shouldInstall = await getShouldInstall(args);
63
- const packageManager = await getPackageManager(args, {root: directory});
77
+ const createAsMonorepo =
78
+ !inWorkspace && (await getCreateAsMonorepo(args, {type: 'package'}));
64
79
  const setupExtras = await getExtrasToSetup(args, {inWorkspace});
80
+ const shouldInstall = await getShouldInstall(args, {type: 'package'});
81
+ const packageManager = await getPackageManager(args, {root: directory});
65
82
 
66
83
  const partOfMonorepo = inWorkspace || createAsMonorepo;
67
84
 
@@ -89,11 +106,18 @@ export async function createProject() {
89
106
 
90
107
  let quiltProject = await packageTemplate.read('quilt.project.ts');
91
108
 
109
+ if (!useReact) {
110
+ quiltProject = quiltProject.replace(
111
+ 'quiltPackage()',
112
+ 'quiltPackage({react: false})',
113
+ );
114
+ }
115
+
92
116
  // If we aren’t already in a workspace, copy the workspace files over, which
93
117
  // are needed if we are making a monorepo or not.
94
118
  if (!inWorkspace) {
95
119
  await workspaceTemplate.copy(directory, (file) => {
96
- // When this is a single project, we use the project’s Quilt configuration as the base.
120
+ // When this is a single project, we use the project’s Quilt configuration as the base.
97
121
  if (file === 'quilt.workspace.ts') return createAsMonorepo;
98
122
 
99
123
  // We need to make some adjustments to the root package.json
@@ -103,11 +127,19 @@ export async function createProject() {
103
127
  // If we are creating a monorepo, we need to add the root package.json and
104
128
  // package manager workspace configuration.
105
129
  if (createAsMonorepo) {
130
+ const packageRelativeToRoot = path.relative(
131
+ rootDirectory,
132
+ packageDirectory,
133
+ );
134
+ const packageGlobRelativeToRoot = relativeDirectoryForDisplay(
135
+ path.join(packageRelativeToRoot, '*'),
136
+ );
106
137
  const workspacePackageJson = JSON.parse(
107
138
  await workspaceTemplate.read('package.json'),
108
139
  );
109
140
 
110
141
  workspacePackageJson.name = toValidPackageName(name!);
142
+ workspacePackageJson.workspaces = [packageGlobRelativeToRoot];
111
143
 
112
144
  if (packageManager.type === 'pnpm') {
113
145
  await outputRoot.write(
@@ -115,13 +147,11 @@ export async function createProject() {
115
147
  await format(
116
148
  `
117
149
  packages:
118
- - './packages/*'
150
+ - '${packageGlobRelativeToRoot}'
119
151
  `,
120
152
  {as: 'yaml'},
121
153
  ),
122
154
  );
123
- } else {
124
- workspacePackageJson.workspaces = ['packages/*'];
125
155
  }
126
156
 
127
157
  await outputRoot.write(
@@ -144,31 +174,14 @@ export async function createProject() {
144
174
  .then((content) => JSON.parse(content)),
145
175
  ]);
146
176
 
147
- workspacePackageJson.eslintConfig = projectPackageJson.eslintConfig;
148
- workspacePackageJson.browserslist = projectPackageJson.browserslist;
149
-
150
- const newPackageJson: Record<string, any> = {};
151
-
152
- // We want to put the project’s dependencies in the package.json, respecting
153
- // the preferred ordering (dependencies, peer dependencies, dev dependencies).
154
- for (const [key, value] of Object.entries(projectPackageJson)) {
155
- if (key !== 'devDependencies') {
156
- newPackageJson[key] = value;
157
- continue;
158
- }
159
-
160
- newPackageJson.dependencies = projectPackageJson.dependencies;
161
- newPackageJson.peerDependencies = projectPackageJson.peerDependencies;
162
- newPackageJson.peerDependenciesMeta =
163
- projectPackageJson.peerDependenciesMeta;
164
- newPackageJson.devDependencies = mergeDependencies(
165
- workspacePackageJson.devDependencies,
166
- projectPackageJson.devDependencies,
167
- );
168
- }
177
+ const mergedPackageJson = mergeWorkspaceAndProjectPackageJsons(
178
+ projectPackageJson,
179
+ workspacePackageJson,
180
+ );
169
181
 
170
- adjustPackageJson(newPackageJson, {
182
+ adjustPackageJson(mergedPackageJson, {
171
183
  name: toValidPackageName(name!),
184
+ description,
172
185
  react: useReact,
173
186
  isPublic,
174
187
  registry: args['--registry'],
@@ -185,7 +198,7 @@ export async function createProject() {
185
198
 
186
199
  await outputRoot.write(
187
200
  'package.json',
188
- await format(JSON.stringify(newPackageJson), {
201
+ await format(JSON.stringify(mergedPackageJson), {
189
202
  as: 'json-stringify',
190
203
  }),
191
204
  );
@@ -221,13 +234,27 @@ export async function createProject() {
221
234
  await packageTemplate.read('package.json'),
222
235
  );
223
236
 
224
- projectPackageJson.repository.directory = path.relative(
225
- rootDirectory,
226
- packageDirectory,
227
- );
237
+ if (repository === false) {
238
+ delete projectPackageJson.repository;
239
+ } else {
240
+ const directory = path.relative(rootDirectory, packageDirectory);
241
+
242
+ if (typeof repository === 'string') {
243
+ projectPackageJson.repository = {
244
+ type: 'git',
245
+ url: repository,
246
+ directory,
247
+ };
248
+ } else if (repository != null) {
249
+ projectPackageJson.repository = {type: 'git', ...repository, directory};
250
+ } else {
251
+ projectPackageJson.repository.directory = directory;
252
+ }
253
+ }
228
254
 
229
255
  adjustPackageJson(projectPackageJson, {
230
256
  name: toValidPackageName(name),
257
+ description,
231
258
  react: useReact,
232
259
  isPublic,
233
260
  registry: args['--registry'],
@@ -256,80 +283,74 @@ export async function createProject() {
256
283
  }
257
284
 
258
285
  if (shouldInstall) {
259
- process.stdout.write('\nInstalling dependencies...\n');
260
286
  // TODO: better loading, handle errors
261
287
  await packageManager.install();
262
- process.stdout.moveCursor(0, -1);
263
- process.stdout.clearLine(1);
264
- console.log('Installed dependencies.');
265
288
  }
266
289
 
267
- const packageJsonInstructions = stripIndent`
268
- Your new package is ready to go! However, before you go too much further,
269
- you should update the following fields in ${color.cyan(
290
+ console.log();
291
+ console.log(
292
+ stripIndent`
293
+ Your new package, ${color.bold(
294
+ name,
295
+ )}, is ready to go! You can edit the code for your package in
296
+ ${color.cyan(
270
297
  relativeDirectoryForDisplay(
271
- path.relative(
272
- process.cwd(),
273
- path.join(packageDirectory, 'package.json'),
274
- ),
298
+ path.relative(process.cwd(), path.join(packageDirectory, 'source')),
275
299
  ),
276
- )}:
277
-
278
- - ${color.bold(
279
- `"description"`,
280
- )}, where you provide a description of what your package does
281
- - ${color.bold(`"repository"`)}, where you should include the ${color.bold(
282
- `"url"`,
283
- )} of your project’s repo
284
-
285
- Before you publish your package, you will also want to update the ${color.bold(
286
- `"version"`,
287
- )}
288
- field in the package.json file.
289
- `;
290
-
291
- console.log();
292
- console.log(packageJsonInstructions);
293
-
294
- const commands: string[] = [];
295
-
296
- if (!inWorkspace && directory !== process.cwd()) {
297
- commands.push(
298
- `cd ${color.cyan(
299
- relativeDirectoryForDisplay(path.relative(process.cwd(), directory)),
300
- )} ${color.dim('# Move into your new package’s directory')}`,
301
- );
302
- }
300
+ )}.
301
+ `,
302
+ );
303
303
 
304
- if (!shouldInstall) {
305
- commands.push(
306
- `${packageManager.commands.install()} ${color.dim(
307
- '# Install all your dependencies',
308
- )}`,
309
- );
310
- }
304
+ if (isPublic) {
305
+ const needsPackageJsonKeys: string[] = [];
311
306
 
312
- if (!inWorkspace) {
313
- // TODO: change this condition to check if git was initialized already
314
- commands.push(
315
- `git init && git add -A && git commit -m "Initial commit" ${color.dim(
316
- '# Start your git history (optional)',
317
- )}`,
318
- );
319
- }
307
+ if (!description) {
308
+ needsPackageJsonKeys.push('description');
309
+ }
320
310
 
321
- if (commands.length > 0) {
322
- const whatsNext = stripIndent`
323
- After you update your package.json, there’s ${
324
- commands.length > 1 ? 'a few more steps' : 'one more step'
325
- } you’ll need to take
326
- in order to start building:
327
- `;
311
+ if (repository == null) {
312
+ needsPackageJsonKeys.push('repository.url');
313
+ }
328
314
 
329
315
  console.log();
330
- console.log(whatsNext);
331
- console.log();
332
- console.log(commands.map((command) => ` ${command}`).join('\n'));
316
+
317
+ if (needsPackageJsonKeys.length > 0) {
318
+ console.log(
319
+ stripIndent`
320
+ Before you publish your package, you will need to add the ${needsPackageJsonKeys
321
+ .map((key) => color.bold(JSON.stringify(key)))
322
+ .join(' and ')} key${needsPackageJsonKeys.length > 1 ? 's' : ''}
323
+ to ${color.cyan(
324
+ relativeDirectoryForDisplay(
325
+ path.relative(
326
+ process.cwd(),
327
+ path.join(packageDirectory, 'package.json'),
328
+ ),
329
+ ),
330
+ )}. In that same file, make sure the contents of the
331
+ ${color.bold('"version"')}, ${color.bold(
332
+ '"exports"',
333
+ )}, and ${color.bold('"license"')} fields are correct for your package.
334
+ `,
335
+ );
336
+ } else {
337
+ console.log(
338
+ stripIndent`
339
+ Before you publish your package, make sure the content of the
340
+ ${color.bold('"version"')}, ${color.bold(
341
+ 'exports',
342
+ )}, and ${color.bold('license')} fields in
343
+ ${color.cyan(
344
+ relativeDirectoryForDisplay(
345
+ path.relative(
346
+ process.cwd(),
347
+ path.join(packageDirectory, 'package.json'),
348
+ ),
349
+ ),
350
+ )} are correct for your package.
351
+ `,
352
+ );
353
+ }
333
354
  }
334
355
 
335
356
  const followUp = stripIndent`
@@ -357,6 +378,10 @@ function getArguments() {
357
378
  '-y': '--yes',
358
379
  '--name': String,
359
380
  '--directory': String,
381
+ '--description': String,
382
+ '--no-description': Boolean,
383
+ '--repository': String,
384
+ '--no-repository': Boolean,
360
385
  '--install': Boolean,
361
386
  '--no-install': Boolean,
362
387
  '--monorepo': Boolean,
@@ -443,6 +468,38 @@ async function getDirectory(
443
468
  return directory;
444
469
  }
445
470
 
471
+ async function getDescription(args: Arguments) {
472
+ if (args['--description']) return args['--description'];
473
+ if (args['--no-description']) return false;
474
+
475
+ const description = await prompt({
476
+ type: 'text',
477
+ message: 'What is a short description of what this package will do?',
478
+ });
479
+
480
+ return description;
481
+ }
482
+
483
+ async function getRepository(args: Arguments, {inWorkspace = false} = {}) {
484
+ if (args['--repository']) return args['--repository'];
485
+ if (args['--no-repository']) return false;
486
+
487
+ if (!inWorkspace) return;
488
+
489
+ const {globby} = await import('globby');
490
+
491
+ const files = await globby('**/package.json', {ignore: ['**/node_modules']});
492
+
493
+ for (const file of files) {
494
+ try {
495
+ const json = JSON.parse(await fs.promises.readFile(file, 'utf8'));
496
+ if (json.repository) return json.repository as string | {url: string};
497
+ } catch {
498
+ // noop
499
+ }
500
+ }
501
+ }
502
+
446
503
  async function getPublic(args: Arguments) {
447
504
  let isPublic: boolean;
448
505
 
@@ -483,10 +540,17 @@ function adjustPackageJson(
483
540
  packageJson: Record<string, any>,
484
541
  {
485
542
  name,
543
+ description,
486
544
  react,
487
545
  isPublic,
488
546
  registry,
489
- }: {name: string; react: boolean; isPublic: boolean; registry?: string},
547
+ }: {
548
+ name: string;
549
+ description: string | false;
550
+ react: boolean;
551
+ isPublic: boolean;
552
+ registry?: string;
553
+ },
490
554
  ) {
491
555
  packageJson.name = name;
492
556
 
@@ -494,6 +558,12 @@ function adjustPackageJson(
494
558
  const scope = packageParts[0]!.startsWith('@') ? packageParts[0] : undefined;
495
559
  const finalRegistry = registry ?? 'https://registry.npmjs.org';
496
560
 
561
+ if (description) {
562
+ packageJson.description = description;
563
+ } else {
564
+ delete packageJson.description;
565
+ }
566
+
497
567
  if (scope) {
498
568
  packageJson.publishConfig[`${scope}/registry`] = finalRegistry;
499
569
  } else if (registry) {
@@ -502,6 +572,8 @@ function adjustPackageJson(
502
572
 
503
573
  if (isPublic) {
504
574
  delete packageJson.private;
575
+ delete packageJson.license;
576
+ delete packageJson.repository;
505
577
  } else {
506
578
  delete packageJson.publishConfig;
507
579
  }
@@ -1,3 +1,5 @@
1
+ import * as fs from 'fs';
2
+
1
3
  import type {Result as ArgvResult} from 'arg';
2
4
  import * as color from 'colorette';
3
5
  import {
@@ -12,6 +14,8 @@ type BaseArguments = ArgvResult<{
12
14
  '--no-install': BooleanConstructor;
13
15
  '--monorepo': BooleanConstructor;
14
16
  '--no-monorepo': BooleanConstructor;
17
+ '--in-workspace': BooleanConstructor;
18
+ '--not-in-workspace': BooleanConstructor;
15
19
  '--extras': [StringConstructor];
16
20
  '--no-extras': BooleanConstructor;
17
21
  '--package-manager': StringConstructor;
@@ -19,7 +23,17 @@ type BaseArguments = ArgvResult<{
19
23
 
20
24
  export {prompt};
21
25
 
22
- export async function getCreateAsMonorepo(argv: BaseArguments) {
26
+ export async function getInWorkspace(argv: BaseArguments) {
27
+ if (argv['--in-workspace']) return true;
28
+ if (argv['--not-in-workspace']) return false;
29
+
30
+ return fs.existsSync('quilt.workspace.ts');
31
+ }
32
+
33
+ export async function getCreateAsMonorepo(
34
+ argv: BaseArguments,
35
+ {type}: {type: 'app' | 'package'},
36
+ ) {
23
37
  let createAsMonorepo: boolean;
24
38
 
25
39
  if (argv['--monorepo' || argv['--yes']]) {
@@ -29,8 +43,7 @@ export async function getCreateAsMonorepo(argv: BaseArguments) {
29
43
  } else {
30
44
  createAsMonorepo = await prompt({
31
45
  type: 'confirm',
32
- message:
33
- 'Do you want to create this app as a monorepo, with room for more projects?',
46
+ message: `Do you want to create this ${type} as a monorepo, with room for more projects?`,
34
47
  initial: true,
35
48
  });
36
49
  }
@@ -38,7 +51,10 @@ export async function getCreateAsMonorepo(argv: BaseArguments) {
38
51
  return createAsMonorepo;
39
52
  }
40
53
 
41
- export async function getShouldInstall(argv: BaseArguments) {
54
+ export async function getShouldInstall(
55
+ argv: BaseArguments,
56
+ {type}: {type: 'app' | 'package'},
57
+ ) {
42
58
  let shouldInstall: boolean;
43
59
 
44
60
  if (argv['--install'] || argv['--yes']) {
@@ -48,8 +64,7 @@ export async function getShouldInstall(argv: BaseArguments) {
48
64
  } else {
49
65
  shouldInstall = await prompt({
50
66
  type: 'confirm',
51
- message:
52
- 'Do you want to install dependencies for this app after creating it?',
67
+ message: `Do you want to install dependencies for this ${type} after creating it?`,
53
68
  initial: true,
54
69
  });
55
70
  }
package/source/shared.ts CHANGED
@@ -152,13 +152,20 @@ export async function format(
152
152
  content: string,
153
153
  {as: parser}: {as: BuiltInParserName},
154
154
  ) {
155
- const [{format}, {default: babel}, {default: typescript}, {default: yaml}] =
156
- await Promise.all([
157
- import('prettier/standalone'),
158
- import('prettier/parser-babel'),
159
- import('prettier/parser-typescript'),
160
- import('prettier/parser-yaml'),
161
- ]);
155
+ const [
156
+ {format: rootFormat, default: prettier},
157
+ {default: babel},
158
+ {default: typescript},
159
+ {default: yaml},
160
+ ] = await Promise.all([
161
+ import('prettier/standalone'),
162
+ import('prettier/parser-babel'),
163
+ import('prettier/parser-typescript'),
164
+ import('prettier/parser-yaml'),
165
+ ]);
166
+
167
+ // CJS workaround
168
+ const format = rootFormat ?? prettier.format;
162
169
 
163
170
  return format(content, {
164
171
  arrowParens: 'always',
@@ -185,3 +192,58 @@ export function mergeDependencies(
185
192
 
186
193
  return merged;
187
194
  }
195
+
196
+ const PACKAGE_JSON_DEPENDENCY_KEYS = new Set([
197
+ 'dependencies',
198
+ 'devDependencies',
199
+ 'peerDependencies',
200
+ 'peerDependenciesMeta',
201
+ ]);
202
+
203
+ // Merges a project and workspace package.json together, with the following nitpicky preferences:
204
+ //
205
+ // - Take all the project’s fields in the order they appear by default
206
+ // - Merge the relevant dependencies together
207
+ // - Projects don’t come with `scripts` by default, but that should go before the first dependency list
208
+ // - If there are other keys in the workspace package.json, they should go last, in the order they appeared
209
+ export function mergeWorkspaceAndProjectPackageJsons(
210
+ projectPackageJson: Record<string, unknown>,
211
+ workspacePackageJson: Record<string, unknown>,
212
+ ) {
213
+ const newPackageJson: Record<string, unknown> = {};
214
+ const seenKeys = new Set<string>();
215
+ let hasHandledScriptsField =
216
+ workspacePackageJson.scripts != null && projectPackageJson.scripts == null;
217
+
218
+ for (const [key, value] of Object.entries(projectPackageJson)) {
219
+ seenKeys.add(key);
220
+
221
+ const isDependencyKey = PACKAGE_JSON_DEPENDENCY_KEYS.has(key);
222
+
223
+ if (key === 'scripts' || (isDependencyKey && !hasHandledScriptsField)) {
224
+ newPackageJson.scripts = {
225
+ ...(workspacePackageJson.scripts as any),
226
+ ...(projectPackageJson.scripts as any),
227
+ };
228
+ hasHandledScriptsField = true;
229
+ }
230
+
231
+ if (isDependencyKey) {
232
+ newPackageJson[key] = mergeDependencies(
233
+ value as any,
234
+ workspacePackageJson[key] as any,
235
+ );
236
+ } else {
237
+ newPackageJson[key] = value;
238
+ }
239
+ }
240
+
241
+ for (const [key, value] of Object.entries(workspacePackageJson)) {
242
+ if (seenKeys.has(key)) continue;
243
+ // Merged workspace + project package.json means we are not in a monorepo
244
+ if (key === 'workspaces') continue;
245
+ newPackageJson[key] = value;
246
+ }
247
+
248
+ return newPackageJson;
249
+ }
@@ -5,15 +5,19 @@ import {
5
5
  } from '@quilted/quilt/server';
6
6
  import createAssetManifest from '@quilted/quilt/magic/app/asset-manifest';
7
7
 
8
- import App from './App';
9
-
10
8
  const router = createRequestRouter();
11
9
 
12
10
  // For all GET requests, render our React application.
13
11
  router.get(
14
- createServerRenderingRequestHandler(() => <App />, {
15
- assets: createAssetManifest(),
16
- }),
12
+ createServerRenderingRequestHandler(
13
+ async () => {
14
+ const {default: App} = await import('./App');
15
+ return <App />;
16
+ },
17
+ {
18
+ assets: createAssetManifest(),
19
+ },
20
+ ),
17
21
  );
18
22
 
19
23
  export default router;
@@ -1,5 +1,5 @@
1
1
  import {createProject, quiltPackage} from '@quilted/craft';
2
2
 
3
3
  export default createProject((project) => {
4
- project.use(quiltPackage({react: false}));
4
+ project.use(quiltPackage());
5
5
  });
@@ -10,6 +10,10 @@
10
10
  "develop": "quilt develop",
11
11
  "build": "quilt build"
12
12
  },
13
+ "workspaces": [
14
+ "./app",
15
+ "./packages/*"
16
+ ],
13
17
  "devDependencies": {
14
18
  "@quilted/browserslist-config": "^0.1.0",
15
19
  "@quilted/craft": "^0.1.0",