@carbon/cli 10.29.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.
Files changed (37) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +57 -0
  3. package/bin/carbon-cli.js +46 -0
  4. package/docker-compose.yml +13 -0
  5. package/package.json +56 -0
  6. package/src/changelog.js +217 -0
  7. package/src/cli.js +39 -0
  8. package/src/commands/bundle/bundlers.js +14 -0
  9. package/src/commands/bundle/javascript.js +142 -0
  10. package/src/commands/bundle.js +67 -0
  11. package/src/commands/changelog.js +70 -0
  12. package/src/commands/check.js +71 -0
  13. package/src/commands/ci-check.js +51 -0
  14. package/src/commands/component.js +130 -0
  15. package/src/commands/contribute/setup.js +232 -0
  16. package/src/commands/contribute/tools/getGitHubClient.js +73 -0
  17. package/src/commands/contribute.js +16 -0
  18. package/src/commands/inline.js +170 -0
  19. package/src/commands/publish.js +274 -0
  20. package/src/commands/release.js +302 -0
  21. package/src/commands/sassdoc/tools.js +405 -0
  22. package/src/commands/sassdoc.js +90 -0
  23. package/src/commands/sync/npm.js +43 -0
  24. package/src/commands/sync/package.js +122 -0
  25. package/src/commands/sync/readme.js +68 -0
  26. package/src/commands/sync/remark/remark-monorepo.js +425 -0
  27. package/src/commands/sync.js +39 -0
  28. package/src/compile.js +26 -0
  29. package/src/component/index.js +47 -0
  30. package/src/component/templates/component.template.js +19 -0
  31. package/src/component/templates/index.template.js +9 -0
  32. package/src/component/templates/mdx.template.mdx +37 -0
  33. package/src/component/templates/story.template.js +22 -0
  34. package/src/component/templates/test.template.js +35 -0
  35. package/src/git.js +38 -0
  36. package/src/logger.js +95 -0
  37. package/src/workspace.js +60 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Copyright IBM Corp. 2019, 2019
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const execa = require('execa');
11
+ const { prompt } = require('inquirer');
12
+ const semver = require('semver');
13
+ const { generate } = require('../changelog');
14
+ const { fetchLatestFromUpstream } = require('../git');
15
+ const { createLogger, displayBanner } = require('../logger');
16
+ const { getPackages } = require('../workspace');
17
+
18
+ const logger = createLogger('publish');
19
+ // Enqueue tasks to run at the end of the command where we want to "clean-up"
20
+ // the environment
21
+ const deferred = [];
22
+ function defer(thunk) {
23
+ deferred.push(thunk);
24
+ }
25
+ async function cleanup() {
26
+ for (let i = deferred.length - 1; i >= 0; i--) {
27
+ const task = deferred[i];
28
+ await task;
29
+ }
30
+ deferred.length = 0;
31
+ }
32
+
33
+ /**
34
+ * Publish is the counterpart to the `release` command and is responsible for
35
+ * taking the current state of the project and publishing it to a given package
36
+ * registry. In addition, this command will handle local operations like
37
+ * creating git tags, making sure npm dist-tag's for packages are correct, and
38
+ * will generate a changelog to be used in a GitHub release.
39
+ *
40
+ * @param {object} tag
41
+ * @param {string} tag.tag
42
+ * @returns {void}
43
+ */
44
+ async function publish({ tag, ...flags }) {
45
+ const { gitRemote, noGitTagVersion, noPush, registry, skipReset } = flags;
46
+ const lastTag = await getLastGitTag();
47
+ const packages = await getPackages();
48
+
49
+ displayBanner();
50
+
51
+ logger.start(`Validating the tag: ${tag}`);
52
+
53
+ if (tag[0] !== 'v') {
54
+ throw new Error(
55
+ `Expected tag name to match vX.Y.Z, instead received: ${tag}`
56
+ );
57
+ }
58
+
59
+ if (!semver.valid(tag.slice(1))) {
60
+ throw new Error(
61
+ `Given tag is not a semantically valid version, received: ${tag}`
62
+ );
63
+ }
64
+
65
+ logger.stop();
66
+
67
+ logger.start('Resetting the project to a known state');
68
+
69
+ if (!skipReset) {
70
+ const type = semver.diff(lastTag, tag);
71
+
72
+ if (type !== 'patch' && type !== 'prepatch') {
73
+ logger.info('Fetching latest from upstream master');
74
+ await fetchLatestFromUpstream();
75
+ }
76
+
77
+ logger.info('Cleaning any local artifacts or node_modules');
78
+ // Make sure that our tooling is defined before running clean
79
+ await execa('yarn', ['install', '--offline']);
80
+ await execa('yarn', ['clean']);
81
+
82
+ logger.info('Installing known dependencies from offline mirror');
83
+ await execa('yarn', ['install', '--offline']);
84
+
85
+ logger.info('Building packages from source');
86
+ await execa('yarn', ['build']);
87
+ }
88
+
89
+ logger.stop();
90
+
91
+ logger.start('Checking project for out-of-sync generated files');
92
+
93
+ const { stdout } = await execa('git', ['status', '--porcelain']);
94
+ if (stdout !== '') {
95
+ throw new Error(
96
+ 'There are generated files that are out-of-sync. Please wait for ' +
97
+ 'these changes to be committed upstream or commit manually'
98
+ );
99
+ }
100
+
101
+ logger.stop();
102
+
103
+ logger.start('Publishing packages');
104
+ logger.info('Logging into npm');
105
+
106
+ try {
107
+ // This command will fail if the user is unauthenticated
108
+ await execa('npm', ['whoami', '--registry', registry]);
109
+ } catch {
110
+ await execa('npm', ['login'], {
111
+ stdio: 'inherit',
112
+ });
113
+ }
114
+
115
+ logger.info(`Setting the npm registry to ${registry}`);
116
+ const { stdout: originalRegistryUrl } = await execa('npm', [
117
+ 'get',
118
+ 'registry',
119
+ ]);
120
+ if (originalRegistryUrl !== registry) {
121
+ await execa('npm', ['set', 'registry', registry]);
122
+ defer(() => execa('npm', ['set', 'registry', originalRegistryUrl]));
123
+ }
124
+
125
+ // Publish packages using `lerna`, we default to placing these under the
126
+ // `next` tag instead of `latest` so that we can verify the release.
127
+ await execa(
128
+ 'yarn',
129
+ [
130
+ 'lerna',
131
+ 'publish',
132
+ 'from-package',
133
+ '--dist-tag',
134
+ 'next',
135
+ '--registry',
136
+ registry,
137
+ ],
138
+ {
139
+ stdio: 'inherit',
140
+ }
141
+ );
142
+
143
+ logger.stop();
144
+
145
+ // We specify a stopping point so that the operator can verify the packages
146
+ // published as intended before setting the `latest` dist-tag
147
+ const answers = await prompt([
148
+ {
149
+ type: 'confirm',
150
+ name: 'tags',
151
+ message: 'Would you like to update the package tags from next to latest?',
152
+ },
153
+ ]);
154
+
155
+ if (answers.tags) {
156
+ logger.start('Setting npm dist tags to latest');
157
+
158
+ for (const { name, version } of packages) {
159
+ logger.info(`Setting npm dist-tag for ${name}@${version} to latest`);
160
+ await execa('npm', ['dist-tag', 'add', `${name}@${version}`, 'latest']);
161
+ }
162
+
163
+ logger.stop();
164
+ }
165
+
166
+ if (!noGitTagVersion) {
167
+ logger.start(`Generating the git tag ${tag}`);
168
+ await execa('git', ['tag', '-a', tag, '-m', tag]);
169
+ logger.stop();
170
+ }
171
+
172
+ if (!noPush) {
173
+ const { remote } = await prompt([
174
+ {
175
+ type: 'confirm',
176
+ name: 'remote',
177
+ message: `Should we push the tag ${tag} to the ${gitRemote} remote?`,
178
+ },
179
+ ]);
180
+
181
+ if (remote) {
182
+ logger.start(`Pushing the tag ${tag} to the ${gitRemote} remote`);
183
+ await execa('git', ['push', gitRemote, tag]);
184
+ logger.stop();
185
+ }
186
+ }
187
+
188
+ logger.start('Generating the changelog');
189
+ logger.info(`Using commits from ${lastTag}...${tag}`);
190
+
191
+ const changelog = await generate(packages, lastTag, tag);
192
+ logger.stop();
193
+
194
+ const { display } = await prompt([
195
+ {
196
+ type: 'confirm',
197
+ name: 'display',
198
+ message: 'Display contents of generated changelog for release?',
199
+ },
200
+ ]);
201
+
202
+ if (display) {
203
+ // eslint-disable-next-line no-console
204
+ console.log(changelog);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Gets the last known git tag from the `git tag` sub-command.
210
+ * @returns {string}
211
+ */
212
+ async function getLastGitTag() {
213
+ const { stdout: tagInfo } = await execa('git', [
214
+ 'tag',
215
+ '-l',
216
+ '--sort=-v:refname',
217
+ ]);
218
+ const tags = tagInfo.split('\n');
219
+ return tags[0];
220
+ }
221
+
222
+ module.exports = {
223
+ command: 'publish <tag>',
224
+ desc:
225
+ 'publish packages that have different versions from the package registry',
226
+ builder(yargs) {
227
+ yargs.positional('tag', {
228
+ describe: 'the version tag associated with the release',
229
+ type: 'string',
230
+ });
231
+
232
+ yargs.option('skip-reset', {
233
+ demandOption: false,
234
+ default: false,
235
+ describe: 'Skip the project reset step',
236
+ type: 'boolean',
237
+ });
238
+
239
+ yargs.option('registry', {
240
+ demandOption: false,
241
+ describe: 'Specify registry URL',
242
+ default: 'https://registry.npmjs.org/',
243
+ type: 'string',
244
+ });
245
+
246
+ yargs.option('no-git-tag-version', {
247
+ demandOption: false,
248
+ describe: 'Do not commit or tag version changes.',
249
+ default: false,
250
+ type: 'boolean',
251
+ });
252
+
253
+ yargs.option('git-remote', {
254
+ demandOption: false,
255
+ describe: 'Push git changes to the specified remote.',
256
+ default: 'upstream',
257
+ type: 'string',
258
+ });
259
+
260
+ yargs.option('no-push', {
261
+ demandOption: false,
262
+ describe: 'Do not push tagged commit to git remote.',
263
+ default: false,
264
+ type: 'boolean',
265
+ });
266
+ },
267
+ async handler(...args) {
268
+ try {
269
+ await publish(...args);
270
+ } finally {
271
+ await cleanup();
272
+ }
273
+ },
274
+ };
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Copyright IBM Corp. 2019, 2019
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const parse = require('@commitlint/parse');
11
+ const execa = require('execa');
12
+ const { prompt } = require('inquirer');
13
+ const semver = require('semver');
14
+ const { fetchLatestFromUpstream } = require('../git');
15
+ const { createLogger, displayBanner } = require('../logger');
16
+
17
+ // All supported commit types from our conventional-changelog preset
18
+ const types = [
19
+ 'build',
20
+ 'ci',
21
+ 'chore',
22
+ 'docs',
23
+ 'feat',
24
+ 'fix',
25
+ 'perf',
26
+ 'refactor',
27
+ 'revert',
28
+ 'style',
29
+ 'test',
30
+ ];
31
+
32
+ // Filter supported commit types per release bump
33
+ const typesByReleaseBump = {
34
+ minor: types,
35
+ patch: types.filter((type) => type !== 'feat'),
36
+ };
37
+
38
+ const logger = createLogger('release');
39
+
40
+ /**
41
+ * Create a branch with the commits necessary to generate a release for the
42
+ * given release bump. This command will execute the commands a developer may
43
+ * run in their terminal, but helps to create consistency around how to
44
+ * determine the appropriate names for tags or branches, it also helps with
45
+ * automatically determining which commits to cherry-pick over from the `master`
46
+ * into the current release branch
47
+ * @param {object} config
48
+ * @param {string} config.bump
49
+ * @returns {void}
50
+ */
51
+ async function release({ bump }) {
52
+ displayBanner();
53
+
54
+ logger.start('Getting latest tag');
55
+
56
+ // Make sure we've fetched the latest tags from upstream
57
+ await execa('git', ['pull', 'upstream', 'master', '--tags']);
58
+ const { stdout: tagInfo } = await execa('git', [
59
+ 'tag',
60
+ '-l',
61
+ '--sort=-v:refname',
62
+ ]);
63
+ const tags = tagInfo.split('\n');
64
+ const [latestTag] = tags;
65
+
66
+ if (!latestTag) {
67
+ throw new Error('Unable to find latest tag from git tags');
68
+ }
69
+
70
+ logger.stop();
71
+ logger.start(`Finding next available version for bump: ${bump}`);
72
+
73
+ const nextTag = semver.inc(latestTag, bump);
74
+
75
+ logger.info(`Bumping ${latestTag} to ${nextTag}`);
76
+ logger.stop();
77
+
78
+ const branchName = `chore/release-${nextTag}`;
79
+ logger.start(`Creating branch: ${branchName}`);
80
+
81
+ if (bump === 'patch') {
82
+ logger.info(`Using tag ${latestTag} as base`);
83
+ // If we're bumping for a patch release, we'll need to base our release off
84
+ // of the previous known tag
85
+ await execa('git', ['checkout', latestTag]);
86
+ } else {
87
+ logger.info(`Using master branch as base`);
88
+ // If we're publishing other releases, we'll need to base our release off of
89
+ // the latest stable `master` branch
90
+ await fetchLatestFromUpstream();
91
+ }
92
+ await execa('git', ['checkout', '-b', branchName]);
93
+
94
+ logger.stop();
95
+
96
+ if (bump === 'patch') {
97
+ const commitRange = `${latestTag}...master`;
98
+ await cherryPickCommitsFrom(commitRange, bump);
99
+ }
100
+
101
+ // After making sure our base branch is up-to-date, let's go ahead and reset
102
+ // our project and rebuild everything from a known state. This is helpful for
103
+ // getting rid of any local inconsistencies. Ultimately this process
104
+ // replicates what we do in our Continuous Integration checks.
105
+ await resetProjectState();
106
+
107
+ // Just in case there are any freshly generated files after running the steps
108
+ // above, we'll check to see if the local branch is dirty before proceeding
109
+ await checkIfBranchIsDirty();
110
+
111
+ // Call out to lerna to handle versioning changed packages
112
+ await execa(
113
+ 'yarn',
114
+ ['lerna', 'version', bump, '--no-push', '--no-git-tag-version', '--exact'],
115
+ {
116
+ stdio: 'inherit',
117
+ }
118
+ );
119
+
120
+ logger.start('Creating final commit');
121
+ logger.info(
122
+ 'The next step will be to manually create a Pull Request for this branch'
123
+ );
124
+
125
+ const versionCommitMessage = 'chore(release): update package versions';
126
+ await execa('git', ['add', '-A']);
127
+ await execa('git', ['commit', '-m', versionCommitMessage]);
128
+
129
+ logger.stop();
130
+ }
131
+
132
+ /**
133
+ * When working with patch releases, we'll want to cherry pick commits that are
134
+ * found in the commit range between two tags. This helper also considers the
135
+ * version bump and the types of commits found. Depending on the bump certain
136
+ * commit types will be included. If an appropriate commit type is not found,
137
+ * we'll prompt the user for whether or not to include it. If a merge conflict
138
+ * occurs, we'll prompt the user to address it before proceeding.
139
+ *
140
+ * @param {string} commitRange - the two tags we'll want to grab commits from.
141
+ * The format should follow `tagA...tagB`, where tagA is older than tagB.
142
+ * @param {string} bump - the version bump
143
+ * @returns {void}
144
+ */
145
+ async function cherryPickCommitsFrom(commitRange, bump) {
146
+ logger.start(`Getting commits to cherry-pick from ${commitRange}`);
147
+
148
+ const { stdout: commitInfo } = await execa('git', [
149
+ '--no-pager',
150
+ 'log',
151
+ '--oneline',
152
+ commitRange,
153
+ ]);
154
+
155
+ const commits = [];
156
+ for (const commit of commitInfo.split('\n').reverse()) {
157
+ const hash = commit.slice(0, 9);
158
+ const body = commit.slice(10);
159
+ const info = await parse(body);
160
+ const result = {
161
+ info,
162
+ hash,
163
+ body,
164
+ };
165
+
166
+ // If we cannot derive the commit type, we'll need to manually confirm if we
167
+ // should include the commit
168
+ if (info.type === null) {
169
+ const answers = await prompt([
170
+ {
171
+ type: 'confirm',
172
+ name: 'confirmed',
173
+ message: `Should we include the commit: ${commit}`,
174
+ },
175
+ ]);
176
+
177
+ // Override the type with "manual" that we can use when we filter to see
178
+ // what commits we need to cherry pick
179
+ if (answers.confirmed) {
180
+ result.info.type = 'manual';
181
+ }
182
+ }
183
+
184
+ commits.push(result);
185
+ }
186
+ const commitsToCherryPick = commits.filter((commit) => {
187
+ return (
188
+ typesByReleaseBump[bump].includes(commit.info.type) ||
189
+ commit.info.type === 'manual'
190
+ );
191
+ });
192
+
193
+ logger.info(`Found ${commitsToCherryPick.length} commits to cherry-pick`);
194
+
195
+ for (const commit of commitsToCherryPick) {
196
+ logger.info(`${commit.hash} ${commit.body}`);
197
+
198
+ try {
199
+ await execa('git', [
200
+ 'cherry-pick',
201
+ '--strategy=recursive',
202
+ '-X',
203
+ 'theirs',
204
+ commit.hash,
205
+ ]);
206
+ } catch (error) {
207
+ if (!error.stderr.startsWith('error: could not apply')) {
208
+ throw error;
209
+ }
210
+ const answers = await prompt([
211
+ {
212
+ type: 'confirm',
213
+ name: 'proceed',
214
+ message:
215
+ `Applying the commit ${commit.hash} lead to a conflict. ` +
216
+ `Please fix the conflict and then confirm to proceed.`,
217
+ default: false,
218
+ },
219
+ ]);
220
+ if (!answers.proceed) {
221
+ await execa('git', ['cherry-pick', '--abort']);
222
+ throw new Error(
223
+ 'Unable to proceed with cherry-picking after failed conflict ' +
224
+ 'resolution attempt'
225
+ );
226
+ }
227
+ }
228
+ }
229
+
230
+ logger.stop();
231
+ }
232
+
233
+ /**
234
+ * When working with multiple local environments, it's helpful to reset the
235
+ * project to a known state. This helper will try and clean everything up so
236
+ * that the environment is clean and good-to-go moving forward. Most of the
237
+ * steps in this method ultimately reflect what we do in Continuous Integration
238
+ * environments, with the addition of a `clean` command to remove generated
239
+ * artifacts locally.
240
+ *
241
+ * @returns {void}
242
+ */
243
+ async function resetProjectState() {
244
+ logger.start('Resetting the project to a known state');
245
+
246
+ logger.info('Cleaning any local artifacts or node_modules');
247
+ // Make sure that our tooling is defined before running clean
248
+ await execa('yarn', ['install', '--offline']);
249
+ await execa('yarn', ['clean']);
250
+
251
+ logger.info('Installing known dependencies from offline mirror');
252
+ await execa('yarn', ['install', '--offline']);
253
+
254
+ logger.info('Building packages from source');
255
+ await execa('yarn', ['build']);
256
+
257
+ logger.stop();
258
+ }
259
+
260
+ /**
261
+ * When working with generated files, sometimes we'll want to check if the
262
+ * working branch is dirty and if the caller wants to commit these files as part
263
+ * of the release process.
264
+ *
265
+ * @returns {void}
266
+ */
267
+ async function checkIfBranchIsDirty() {
268
+ const { stdout } = await execa('git', ['status', '--porcelain']);
269
+ if (stdout !== '') {
270
+ const { confirmed } = await prompt([
271
+ {
272
+ type: 'confirm',
273
+ name: 'confirmed',
274
+ message:
275
+ 'The git status of the project is currently not clean. Would ' +
276
+ 'you like to commit these changes to the project?',
277
+ },
278
+ ]);
279
+
280
+ if (confirmed) {
281
+ await execa('git', ['add', '-A']);
282
+ await execa('git', [
283
+ 'commit',
284
+ '-m',
285
+ 'chore(project): sync generated files [skip ci]',
286
+ ]);
287
+ }
288
+ }
289
+ }
290
+
291
+ module.exports = {
292
+ command: 'release [bump]',
293
+ desc: 'run the release step for the given version bump',
294
+ builder(yargs) {
295
+ yargs.positional('bump', {
296
+ describe: 'choose a release version to bump',
297
+ choices: ['minor', 'patch'],
298
+ default: 'patch',
299
+ });
300
+ },
301
+ handler: release,
302
+ };