@w5s/mrm-preset 1.0.0-alpha.1

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 (51) hide show
  1. package/.eslintignore +1 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +56 -0
  4. package/bootstrap/README.md +5 -0
  5. package/bootstrap/index.js +97 -0
  6. package/ci/_gitlab/AutoDevops.gitlab-ci.yml +77 -0
  7. package/ci/_gitlab/AutoDevopsInclude.gitlab-ci.yml +251 -0
  8. package/ci/_gitlab/README.md +20 -0
  9. package/ci/_gitlab/Renovate.gitlab-ci.yml +30 -0
  10. package/ci/gitlab.js +38 -0
  11. package/ci/index.js +14 -0
  12. package/commitlint/index.js +40 -0
  13. package/config.json +20 -0
  14. package/contributing/index.js +26 -0
  15. package/contributing/templates/CODE_OF_CONDUCT.md +134 -0
  16. package/core/block.js +77 -0
  17. package/core/commitlint.js +39 -0
  18. package/core/cspell.js +66 -0
  19. package/core/eslint.js +22 -0
  20. package/core/file.js +29 -0
  21. package/core/git.js +73 -0
  22. package/core/githooks.js +71 -0
  23. package/core/gitlabCI.js +8 -0
  24. package/core/jest.js +105 -0
  25. package/core/jsonFile.js +65 -0
  26. package/core/lintStaged.js +36 -0
  27. package/core/npm.js +271 -0
  28. package/core/pkg.js +97 -0
  29. package/core/project.js +58 -0
  30. package/core/semanticRelease.js +43 -0
  31. package/core/typedoc.js +82 -0
  32. package/core/vscode.js +92 -0
  33. package/core/workspace.js +6 -0
  34. package/cspell/index.js +38 -0
  35. package/editorconfig/index.js +107 -0
  36. package/eslint/index.js +127 -0
  37. package/githooks/index.js +62 -0
  38. package/githooks/templates/CODEOWNERS +4 -0
  39. package/gitignore/index.js +25 -0
  40. package/gitignore/template.js +145 -0
  41. package/jest/index.js +19 -0
  42. package/lang/.eslintrc.json +8 -0
  43. package/lang/index.js +143 -0
  44. package/lang/templates/index.spec.ts +7 -0
  45. package/lang/templates/index.ts +6 -0
  46. package/package.json +48 -0
  47. package/postconfigure/index.js +17 -0
  48. package/project/index.js +232 -0
  49. package/release/index.js +29 -0
  50. package/renovate/index.js +61 -0
  51. package/tsconfig.json +13 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @typedef {string|boolean|number|null|Array<unknown>|Record<string, unknown>} JsonValue
3
+ */
4
+
5
+ /**
6
+ * @param {import('mrm-core').Json} jsonFile
7
+ * @param {undefined | string | string[]} path
8
+ */
9
+ function getValue(jsonFile, path) {
10
+ if (path == null) {
11
+ return jsonFile.get();
12
+ }
13
+
14
+ return jsonFile.get(path);
15
+ }
16
+
17
+ /**
18
+ * @param {import('mrm-core').Json} jsonFile
19
+ * @param {undefined | string | string[]} path
20
+ * @param {unknown} pathValue
21
+ */
22
+ function setValue(jsonFile, path, pathValue) {
23
+ if (path == null) {
24
+ jsonFile.set(pathValue);
25
+ } else {
26
+ jsonFile.set(path, pathValue);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Set state of a json value
32
+ *
33
+ * @template {undefined|JsonValue} T
34
+ * @param {import('mrm-core').Json} jsonFile
35
+ * @param {{
36
+ * path: undefined | string | string[],
37
+ * state: 'present'|'absent',
38
+ * update?: T | ((previousValue: T) => T)
39
+ * default?: T | (() => T)
40
+ * }} options
41
+ */
42
+ function value(jsonFile, { state, path, default: defaultValue, update: nextValue }) {
43
+ if (state === 'present') {
44
+ if (nextValue != null) {
45
+ const resolvedValue = typeof nextValue === 'function' ? nextValue(getValue(jsonFile, path)) : nextValue;
46
+ if (resolvedValue != null) {
47
+ setValue(jsonFile, path, resolvedValue);
48
+ }
49
+ }
50
+
51
+ const currentValue = getValue(jsonFile, path);
52
+ if (currentValue == null) {
53
+ const resolvedValue = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
54
+ if (resolvedValue != null) {
55
+ setValue(jsonFile, path, resolvedValue);
56
+ }
57
+ }
58
+ } else if (path) {
59
+ jsonFile.unset(path);
60
+ }
61
+ }
62
+
63
+ module.exports = {
64
+ value,
65
+ };
@@ -0,0 +1,36 @@
1
+ const pkg = require('./pkg');
2
+ const npm = require('./npm');
3
+ const jsonFile = require('./jsonFile');
4
+
5
+ /**
6
+ * @typedef {Record<string, string|string[]>} LintStagedConfig
7
+ */
8
+
9
+ /**
10
+ * @param {{
11
+ * state: 'present'|'absent',
12
+ * update?: (config: LintStagedConfig) => LintStagedConfig
13
+ * }} options
14
+ */
15
+ function lintStaged({ state, update }) {
16
+ pkg.withPackageJson((packageFile) => {
17
+ jsonFile.value(packageFile, {
18
+ path: 'lint-staged',
19
+ state,
20
+ update,
21
+ /** @type {LintStagedConfig} */
22
+ default: {},
23
+ });
24
+ });
25
+
26
+ // Dependencies
27
+ npm.dependency({
28
+ dev: true,
29
+ name: ['lint-staged'],
30
+ state,
31
+ });
32
+ }
33
+
34
+ module.exports = {
35
+ lintStaged,
36
+ };
package/core/npm.js ADDED
@@ -0,0 +1,271 @@
1
+ /* eslint-disable import/no-extraneous-dependencies */
2
+ /* cSpell: disable */
3
+ // @ts-check
4
+ // @ts-ignore
5
+ const fs = require('fs-extra');
6
+ // @ts-ignore
7
+ const _ = require('lodash');
8
+ // @ts-ignore
9
+ const semver = require('semver');
10
+ // @ts-ignore
11
+ const listify = require('listify');
12
+ // @ts-ignore
13
+ const validateNpmPackageName = require('validate-npm-package-name');
14
+ // @ts-ignore
15
+ const log = require('mrm-core/src/util/log');
16
+ // @ts-ignore
17
+ const execCommand = require('mrm-core/src/util/execCommand');
18
+ // @ts-ignore
19
+ const json = require('mrm-core/src/formats/json');
20
+ // @ts-ignore
21
+ const packageJson = require('mrm-core/src/files/packageJson');
22
+ // @ts-ignore
23
+ const MrmError = require('mrm-core/src/error');
24
+
25
+ /**
26
+ * @typedef Options
27
+ * @property {boolean} [dev]
28
+ * @property {boolean} [yarn]
29
+ * @property {Record<string, string>} [versions]
30
+ */
31
+
32
+ /**
33
+ * @typedef RunOptions
34
+ * @property {boolean} [dev]
35
+ * @property {boolean} [remove]
36
+ * @property {boolean} [stdio]
37
+ * @property {string} [cwd]
38
+ */
39
+
40
+ /**
41
+ * Install or update given npm packages if needed
42
+ *
43
+ * @param {Record<string, string> | string[] | string} deps
44
+ * @param {Options} [options]
45
+ * @param {Function=} exec
46
+ */
47
+ function install(deps, options = {}, exec) {
48
+ const dev = options.dev !== false;
49
+ const run = options.yarn || isUsingYarn() ? runYarn : runNpm;
50
+
51
+ // options.versions is a min versions mapping,
52
+ // the list of packages to install will be taken from deps
53
+ let versions = options.versions || {};
54
+
55
+ /** @type string[] */
56
+ let dependencies = [];
57
+
58
+ if (typeof deps === 'string') {
59
+ dependencies = [deps];
60
+ } else if (Array.isArray(deps)) {
61
+ dependencies = deps;
62
+ } else if (typeof deps === 'object' && deps !== null) {
63
+ // deps is an object with required versions
64
+ // prettier-ignore
65
+ versions = deps;
66
+ dependencies = Object.keys(deps);
67
+ }
68
+
69
+ const newDeps = getUnsatisfiedDeps(dependencies, versions, { dev });
70
+ if (newDeps.length === 0) {
71
+ return;
72
+ }
73
+
74
+ log.info(`Installing ${listify(newDeps)}...`);
75
+ const versionedDeps = newDeps.map((dep) => {
76
+ return getVersionedDep(dep, versions);
77
+ });
78
+
79
+ // eslint-disable-next-line consistent-return
80
+ return run(versionedDeps, { dev }, exec);
81
+ }
82
+
83
+ /**
84
+ * Uninstall given npm packages
85
+ *
86
+ * @param {string[] | string} deps
87
+ * @param {Options} [options]
88
+ * @param {Function=} exec
89
+ */
90
+ function uninstall(deps, options = {}, exec) {
91
+ // eslint-disable-next-line no-param-reassign
92
+ deps = _.castArray(deps);
93
+ const dev = options.dev !== false;
94
+ const run = options.yarn || isUsingYarn() ? runYarn : runNpm;
95
+
96
+ const installed = getOwnDependencies({ dev });
97
+
98
+ // @ts-ignore
99
+ const newDeps = deps.filter((dep) => {
100
+ return installed[dep];
101
+ });
102
+
103
+ if (newDeps.length === 0) {
104
+ return;
105
+ }
106
+
107
+ log.info(`Uninstalling ${listify(newDeps)}...`);
108
+
109
+ // eslint-disable-next-line consistent-return
110
+ return run(newDeps, { dev, remove: true }, exec);
111
+ }
112
+
113
+ /**
114
+ * Install given npm packages
115
+ *
116
+ * @param {string[]} deps
117
+ * @param {RunOptions} [options]
118
+ * @param {Function} [exec]
119
+ */
120
+ function runNpm(deps, options = {}, exec) {
121
+ const args = [options.remove ? 'uninstall' : 'install', options.dev ? '--save-dev' : '--save'].concat(deps);
122
+
123
+ return execCommand(exec, 'npm', args, {
124
+ cwd: options.cwd,
125
+ stdio: options.stdio === undefined ? 'inherit' : options.stdio,
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Install given Yarn packages
131
+ *
132
+ * @param {string[]} deps
133
+ * @param {RunOptions} [options]
134
+ * @param {Function} [exec]
135
+ */
136
+ function runYarn(deps, options = {}, exec) {
137
+ const add = options.dev ? ['add', '--dev'] : ['add'];
138
+ const remove = ['remove'];
139
+ const args = (options.remove ? remove : add).concat(isUsingWorkspaces() ? ['-W'] : []).concat(deps);
140
+
141
+ return execCommand(exec, 'yarn', args, {
142
+ cwd: options.cwd,
143
+ stdio: options.stdio === undefined ? 'inherit' : options.stdio,
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Add version or latest to package name
149
+ *
150
+ * @param {string} dep
151
+ * @param {Record<string, string>} versions
152
+ */
153
+ function getVersionedDep(dep, versions) {
154
+ // Handle non-registry packages (Github, bitbucket, etc.)
155
+ if (!validateNpmPackageName(dep).validForNewPackages) {
156
+ // If we were explicitly passed a version, attempt to
157
+ // load it via the `#semver:<semver>` syntax.
158
+ if (versions[dep]) {
159
+ return `${dep}#semver:${versions[dep]}`;
160
+ }
161
+
162
+ return dep;
163
+ }
164
+ const version = versions[dep] || 'latest';
165
+
166
+ return `${dep}@${version}`;
167
+ }
168
+
169
+ /**
170
+ *
171
+ * @param {Options} options
172
+ * @returns {Record<string, string>}
173
+ */
174
+ function getOwnDependencies(options) {
175
+ const pkg = packageJson({
176
+ dependencies: {},
177
+ devDependencies: {},
178
+ });
179
+
180
+ return pkg.get(options.dev ? 'devDependencies' : 'dependencies') || {};
181
+ }
182
+
183
+ /**
184
+ * Return version of installed npm package
185
+ *
186
+ * @param {string} name
187
+ * @returns {string}
188
+ */
189
+ function getInstalledVersion(name) {
190
+ return json(`./node_modules/${name}/package.json`).get('version');
191
+ }
192
+
193
+ /**
194
+ * Return only not installed dependencies, or dependencies which installed
195
+ * version doesn't satisfy range.
196
+ *
197
+ * @param {string[]} deps
198
+ * @param {Record<string, string>} versions
199
+ * @param {Options} options
200
+ * @returns {string[]}
201
+ */
202
+ function getUnsatisfiedDeps(deps, versions, options) {
203
+ const ownDependencies = getOwnDependencies(options);
204
+
205
+ return deps.filter((dep) => {
206
+ const required = versions[dep];
207
+
208
+ // Handle non-registry packages (github, bitbucket, etc.)
209
+ // Because these packages can shift contents without updating version
210
+ // numbers, always attempt an install
211
+ if (!validateNpmPackageName(dep).validForNewPackages) {
212
+ return true;
213
+ }
214
+
215
+ if (required && !semver.validRange(required)) {
216
+ throw new MrmError(`Invalid npm version: ${required}. Use proper semver range syntax.`);
217
+ }
218
+
219
+ const installed = getInstalledVersion(dep);
220
+
221
+ // Package isn’t installed yet
222
+ if (!installed) {
223
+ return true;
224
+ }
225
+
226
+ // Module is installed but not in package.json dependencies
227
+ if (!ownDependencies[dep]) {
228
+ return true;
229
+ }
230
+
231
+ // No required version specified
232
+ if (!required) {
233
+ // Install if the pacakge isn’t installed
234
+ return !installed;
235
+ }
236
+
237
+ // Install if installed version doesn't satisfy range
238
+ return !semver.satisfies(installed, required);
239
+ });
240
+ }
241
+
242
+ /*
243
+ * Is project using Yarn?
244
+ */
245
+ function isUsingYarn() {
246
+ return fs.existsSync('yarn.lock');
247
+ }
248
+
249
+ function isUsingWorkspaces() {
250
+ return Boolean(packageJson().get('workspaces'));
251
+ }
252
+
253
+ /**
254
+ * @param {{
255
+ * name: string|string[]|Record<string, string>,
256
+ * state: 'present'|'absent',
257
+ * dev?: boolean,
258
+ * yarn?: boolean,
259
+ * }} options
260
+ */
261
+ function dependency({ name, state, ...options }) {
262
+ if (state === 'present') {
263
+ install(name, options);
264
+ } else {
265
+ uninstall(typeof name === 'string' || Array.isArray(name) ? name : Object.keys(name));
266
+ }
267
+ }
268
+
269
+ module.exports = {
270
+ dependency,
271
+ };
package/core/pkg.js ADDED
@@ -0,0 +1,97 @@
1
+ /* cSpell: disable */
2
+ // @ts-check
3
+
4
+ // @ts-ignore
5
+ const { intersect } = require('semver-intersect');
6
+ const { packageJson, file } = require('mrm-core');
7
+ const jsonFile = require('./jsonFile');
8
+
9
+ /**
10
+ * An empty placeholder for npm script
11
+ */
12
+ const emptyScript = ':';
13
+
14
+ /**
15
+ * @param {(pkg: import('mrm-core').PackageJson) => void} block
16
+ */
17
+ function withPackageJson(block) {
18
+ const packageFile = packageJson();
19
+ block(packageFile);
20
+ packageFile.save();
21
+ }
22
+
23
+ /**
24
+ * @param {import('mrm-core').PackageJson} packageFile
25
+ * @param {{
26
+ * name: string,
27
+ * state: 'present'|'absent'|'default',
28
+ * script: string,
29
+ * }} options
30
+ */
31
+ function script(packageFile, { name, state, script: scriptName }) {
32
+ if (state === 'absent') {
33
+ packageFile.removeScript(name);
34
+ } else if (state === 'present' || (state === 'default' && !packageFile.getScript(name))) {
35
+ packageFile.setScript(name, scriptName);
36
+ }
37
+ }
38
+
39
+ const defaultManager = 'npm';
40
+
41
+ /**
42
+ * @param {import('mrm-core').PackageJson} packageFile
43
+ * @returns {'yarn'|'npm'}
44
+ */
45
+ function manager(packageFile) {
46
+ if (packageFile.get('packagerManager')) {
47
+ const [head, tail] = packageFile.get('packagerManager').split('@', 2);
48
+
49
+ return head ?? tail;
50
+ }
51
+ if (file('yarn.lock').exists()) {
52
+ return 'yarn';
53
+ }
54
+ if (file('package-json.lock').exists()) {
55
+ return 'npm';
56
+ }
57
+
58
+ return defaultManager;
59
+ }
60
+
61
+ /**
62
+ *
63
+ * @param {import('mrm-core').PackageJson} packageFile
64
+ * @param {Record<string, string>} engineVersionMap
65
+ */
66
+ function engineMinVersion(packageFile, engineVersionMap) {
67
+ /**
68
+ * @param {string} engineName
69
+ */
70
+ const engineConstraint = (engineName) => {
71
+ const defaultVersion = engineVersionMap[engineName];
72
+ const currentVersion = packageFile.get(`engines.${engineName}`, defaultVersion);
73
+ try {
74
+ return intersect(currentVersion, defaultVersion);
75
+ } catch (_) {
76
+ return currentVersion; // leave unchanged
77
+ }
78
+ };
79
+
80
+ packageFile.merge({
81
+ engines: Object.keys(engineVersionMap).reduce((acc, engineName) => {
82
+ return {
83
+ ...acc,
84
+ [engineName]: engineConstraint(engineName),
85
+ };
86
+ }, {}),
87
+ });
88
+ }
89
+
90
+ module.exports = {
91
+ ...jsonFile,
92
+ emptyScript,
93
+ script,
94
+ manager,
95
+ engineMinVersion,
96
+ withPackageJson,
97
+ };
@@ -0,0 +1,58 @@
1
+ /** @type {'install'} */
2
+ const install = 'install';
3
+ /** @type {'prepare'} */
4
+ const prepare = 'prepare';
5
+ /** @type {'build'} */
6
+ const build = 'build';
7
+ /** @type {'develop'} */
8
+ const develop = 'develop';
9
+ /** @type {'code-analysis'} */
10
+ const codeAnalysis = 'code-analysis';
11
+ /** @type {'format'} */
12
+ const format = 'format';
13
+ /** @type {'lint'} */
14
+ const lint = 'lint';
15
+ /** @type {'test'} */
16
+ const test = 'test';
17
+ /** @type {'coverage'} */
18
+ const coverage = 'coverage';
19
+ /** @type {'clean'} */
20
+ const clean = 'clean';
21
+ /** @type {'validate'} */
22
+ const validate = 'validate';
23
+ /** @type {'release'} */
24
+ const release = 'release';
25
+ /** @type {'rescue'} */
26
+ const rescue = 'rescue';
27
+
28
+ /**
29
+ * @param {string} taskName
30
+ */
31
+ function pre(taskName) {
32
+ return `pre${taskName}`;
33
+ }
34
+
35
+ /**
36
+ * @param {string} taskName
37
+ */
38
+ function post(taskName) {
39
+ return `post${taskName}`;
40
+ }
41
+
42
+ module.exports = {
43
+ build,
44
+ prepare,
45
+ develop,
46
+ clean,
47
+ codeAnalysis,
48
+ coverage,
49
+ format,
50
+ install,
51
+ lint,
52
+ post,
53
+ pre,
54
+ release,
55
+ rescue,
56
+ test,
57
+ validate,
58
+ };
@@ -0,0 +1,43 @@
1
+ const npm = require('./npm');
2
+ const pkg = require('./pkg');
3
+ const jsonFile = require('./jsonFile');
4
+
5
+ /**
6
+ * @typedef {{
7
+ * extends: string|string[]
8
+ * }} SemanticReleaseConfig
9
+ */
10
+
11
+ /**
12
+ * @param {{
13
+ * state: 'present'|'absent',
14
+ * preset?: string,
15
+ * update?: (config: SemanticReleaseConfig) => SemanticReleaseConfig
16
+ * }} options
17
+ */
18
+ function semanticRelease({ state, update, preset }) {
19
+ npm.dependency({
20
+ dev: true,
21
+ name: ['semantic-release', ...(preset ? [preset] : [])],
22
+ state,
23
+ });
24
+
25
+ pkg.withPackageJson((packageFile) => {
26
+ jsonFile.value(packageFile, {
27
+ path: 'release',
28
+ state,
29
+ update,
30
+ /** @type {SemanticReleaseConfig} */
31
+ default: {
32
+ extends: preset ? [preset] : [],
33
+ },
34
+ });
35
+ });
36
+ }
37
+ semanticRelease.command = function () {
38
+ return 'semantic-release';
39
+ };
40
+
41
+ module.exports = {
42
+ semanticRelease,
43
+ };
@@ -0,0 +1,82 @@
1
+ const { packageJson, json } = require('mrm-core');
2
+ const pkg = require('./pkg');
3
+ const npm = require('./npm');
4
+ const jsonFile = require('./jsonFile');
5
+
6
+ /**
7
+ * @typedef {{
8
+ * out?: string,
9
+ * tsconfig?: string,
10
+ * exclude?: Array<string>,
11
+ * excludePrivate?: boolean,
12
+ * excludeExternals?: boolean,
13
+ * readme?: string,
14
+ * theme?: string,
15
+ * }} TypeDocConfig
16
+ */
17
+
18
+ /**
19
+ * @param {{
20
+ * state: 'present'|'absent',
21
+ * update: (config: TypeDocConfig) => TypeDocConfig
22
+ * }} options
23
+ */
24
+ function typedoc({ state, update }) {
25
+ const packageFileDefault = packageJson();
26
+ const hasWorkspaces = Boolean(packageFileDefault.get('workspaces'));
27
+ const hasTypedoc = state === 'present';
28
+
29
+ pkg.withPackageJson((packageFile) => {
30
+ pkg.script(packageFile, {
31
+ name: 'typedoc',
32
+ script: 'typedoc',
33
+ state: !hasTypedoc || hasWorkspaces ? 'absent' : 'present',
34
+ });
35
+ });
36
+
37
+ const typedocFile = json('typedoc.json');
38
+
39
+ if (hasTypedoc) {
40
+ jsonFile.value(typedocFile, {
41
+ path: undefined,
42
+ state,
43
+ update: (config) => ({
44
+ ...update(config),
45
+ ...(hasWorkspaces
46
+ ? {
47
+ 'external-modulemap': '.*packages/([^/]+)/.*',
48
+ entryPoints: ['packages/'],
49
+ }
50
+ : {
51
+ 'external-modulemap': undefined,
52
+ entryPoints: ['src/index.ts'],
53
+ }),
54
+ }),
55
+ /** @type {TypeDocConfig} */
56
+ default: {},
57
+ });
58
+
59
+ /**
60
+ * Otherwise save the file with content
61
+ */
62
+ typedocFile.save();
63
+ } else {
64
+ typedocFile.delete();
65
+ }
66
+
67
+ // Dependencies
68
+ npm.dependency({
69
+ dev: true,
70
+ name: ['typedoc'],
71
+ state: hasTypedoc ? 'present' : 'absent',
72
+ });
73
+ npm.dependency({
74
+ dev: true,
75
+ name: ['@strictsoftware/typedoc-plugin-monorepo'],
76
+ state: hasTypedoc && hasWorkspaces ? 'present' : 'absent',
77
+ });
78
+ }
79
+
80
+ module.exports = {
81
+ typedoc,
82
+ };