@amedia/create-kragl 0.0.0-stage → 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @amedia/create-kragl
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 94884aa: Add `@amedia/create-kragl`, which sets up kragl in a project with `npm init @amedia/kragl@latest`: it installs the packages, writes the config files and adds the lint and format scripts.
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # @amedia/create-kragl
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sets up [kragl](../kragl) in a project:
4
+
5
+ ```sh
6
+ npm init @amedia/kragl@latest
7
+ ```
8
+
9
+ In the current directory it:
10
+
11
+ 1. Creates a `package.json` if there is none, as an ES module package.
12
+ 2. Installs `@amedia/kragl` and `@amedia/kragl-lockfile-lint` as exact
13
+ dev dependencies.
14
+ 3. Writes `eslint.config.js` and `prettier.config.js` with `kragl install`.
15
+ 4. Adds the `format`, `lint` and `lint:fix` scripts.
16
+
17
+ Running it again only adds what is missing. It never changes an existing
18
+ script or dependency, and keeps existing config files unless `--force` is
19
+ given. Only npm projects are supported: it stops if it finds a Yarn, pnpm or
20
+ Bun lockfile.
21
+
22
+ `@latest` makes npm fetch the newest version instead of a copy installed
23
+ earlier.
24
+
25
+ ## Options
26
+
27
+ npm passes options after `--` on to `create-kragl`:
28
+
29
+ ```sh
30
+ npm init @amedia/kragl@latest -- --biome --ls-lint
31
+ ```
32
+
33
+ | Option | Effect |
34
+ | -------------------- | -------------------------------------------------------------------------------------------- |
35
+ | `--biome` | Use [Biome](../kragl-biome): installs `@amedia/kragl-cli` and `@amedia/kragl-biome` instead. |
36
+ | `--no-lockfile-lint` | Leave out [lockfile-lint](../kragl-lockfile-lint). |
37
+ | `--ls-lint` | Add [ls-lint](../kragl-ls-lint). |
38
+ | `--browserslist` | Add [Browserslist](../kragl-browserslist). |
39
+ | `-f, --force` | Overwrite existing config files. |
40
+
41
+ A project that already has `@amedia/kragl-biome` stays on Biome without
42
+ `--biome`.
43
+
44
+ ## Notes
45
+
46
+ Requires Node 22+.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/cli.js';
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@amedia/create-kragl",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Set up kragl in a project with `npm init @amedia/kragl`",
5
+ "bin": {
6
+ "create-kragl": "bin/create-kragl.js"
7
+ },
8
+ "type": "module",
9
+ "files": [
10
+ "*"
11
+ ],
12
+ "scripts": {
13
+ "test": "node --test 'src/*-test.js'"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/amedia/eslint-config-amedia.git"
18
+ },
19
+ "keywords": [
20
+ "kragl"
21
+ ],
22
+ "author": "Amedia",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/amedia/eslint-config-amedia/issues"
26
+ },
27
+ "homepage": "https://github.com/amedia/eslint-config-amedia#readme",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "engines": {
32
+ "node": ">=22"
33
+ }
34
+ }
package/src/cli.js ADDED
@@ -0,0 +1,154 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import process from 'node:process';
4
+ import { parseArgs } from 'node:util';
5
+
6
+ import { plan } from './plan.js';
7
+
8
+ const HELP = `Usage: npm init @amedia/kragl@latest -- [options]
9
+
10
+ Installs kragl in the project in the current directory, writes its config
11
+ files and adds the lint and format scripts. Running it again only adds what
12
+ is missing.
13
+
14
+ Options:
15
+ --biome use Biome instead of ESLint and Prettier
16
+ --no-lockfile-lint leave out the lockfile-lint runner
17
+ --ls-lint add the ls-lint runner
18
+ --browserslist add the Browserslist runner
19
+ -f, --force overwrite existing config files
20
+ -h, --help print this help text`;
21
+
22
+ // Lockfiles of the package managers kragl does not support.
23
+ const FOREIGN_LOCKFILES = [
24
+ 'yarn.lock',
25
+ 'pnpm-lock.yaml',
26
+ 'bun.lock',
27
+ 'bun.lockb',
28
+ ];
29
+
30
+ const fail = (message) => {
31
+ console.error(`create-kragl: ${message}`);
32
+ process.exit(1);
33
+ };
34
+
35
+ const npm = (args) =>
36
+ spawnSync('npm', args, {
37
+ stdio: 'inherit',
38
+ shell: process.platform === 'win32',
39
+ }).status === 0;
40
+
41
+ const readPackageJson = () => JSON.parse(readFileSync('package.json', 'utf8'));
42
+
43
+ // Keeps the file's own indentation, the way npm does when it writes it.
44
+ const addScripts = (scripts) => {
45
+ const text = readFileSync('package.json', 'utf8');
46
+ const indent = text.match(/^[ \t]+/m)?.[0] ?? 2;
47
+ const packageJson = JSON.parse(text);
48
+
49
+ packageJson.scripts = { ...packageJson.scripts, ...scripts };
50
+ writeFileSync(
51
+ 'package.json',
52
+ `${JSON.stringify(packageJson, null, indent)}\n`
53
+ );
54
+ };
55
+
56
+ let values;
57
+
58
+ try {
59
+ ({ values } = parseArgs({
60
+ options: {
61
+ biome: { type: 'boolean', default: false },
62
+ 'no-lockfile-lint': { type: 'boolean', default: false },
63
+ 'ls-lint': { type: 'boolean', default: false },
64
+ browserslist: { type: 'boolean', default: false },
65
+ force: { type: 'boolean', short: 'f', default: false },
66
+ help: { type: 'boolean', short: 'h', default: false },
67
+ },
68
+ }));
69
+ } catch (error) {
70
+ fail(`${error.message}\n\n${HELP}`);
71
+ }
72
+
73
+ if (values.help) {
74
+ console.log(HELP);
75
+ process.exit(0);
76
+ }
77
+
78
+ const foreign = FOREIGN_LOCKFILES.find((file) => existsSync(file));
79
+
80
+ if (foreign) {
81
+ fail(`found ${foreign}; kragl supports npm projects only.`);
82
+ }
83
+
84
+ // The config files kragl writes are ES modules, and npm 11 makes new packages
85
+ // CommonJS unless told otherwise.
86
+ if (
87
+ !existsSync('package.json') &&
88
+ !npm(['init', '--yes', '--init-type=module'])
89
+ ) {
90
+ fail('npm init failed.');
91
+ }
92
+
93
+ const { packages, configs, scripts } = plan(readPackageJson(), {
94
+ biome: values.biome,
95
+ 'lockfile-lint': !values['no-lockfile-lint'],
96
+ 'ls-lint': values['ls-lint'],
97
+ browserslist: values.browserslist,
98
+ });
99
+
100
+ if (
101
+ packages.length > 0 &&
102
+ !npm(['install', '--save-dev', '--save-exact', ...packages])
103
+ ) {
104
+ fail('npm install failed.');
105
+ }
106
+
107
+ // Existing config files are kept unless --force. `--no` makes npm use the
108
+ // kragl just installed and never fetch one from the registry.
109
+ const kept = values.force
110
+ ? []
111
+ : configs.filter(({ filename }) => existsSync(filename));
112
+ const failed = configs
113
+ .filter((config) => !kept.includes(config))
114
+ .filter(
115
+ ({ tool }) =>
116
+ !npm([
117
+ 'exec',
118
+ '--no',
119
+ '--',
120
+ 'kragl',
121
+ 'install',
122
+ `--${tool}`,
123
+ ...(values.force ? ['--force'] : []),
124
+ ])
125
+ );
126
+
127
+ const scriptNames = Object.keys(scripts);
128
+
129
+ if (scriptNames.length > 0) {
130
+ addScripts(scripts);
131
+ }
132
+
133
+ console.log('');
134
+ console.log(
135
+ packages.length > 0
136
+ ? `Installed ${packages.join(', ')}.`
137
+ : 'All kragl packages were already installed.'
138
+ );
139
+
140
+ if (scriptNames.length > 0) {
141
+ console.log(`Added scripts: ${scriptNames.join(', ')}.`);
142
+ }
143
+
144
+ if (kept.length > 0) {
145
+ const filenames = kept.map(({ filename }) => filename).join(', ');
146
+ console.log(`Kept ${filenames}; re-run with --force to replace.`);
147
+ }
148
+
149
+ if (failed.length > 0) {
150
+ const tools = failed.map(({ tool }) => tool).join(', ');
151
+ fail(`kragl install failed for ${tools}, see above.`);
152
+ }
153
+
154
+ console.log('Run `npm run lint` to lint the project.');
@@ -0,0 +1,76 @@
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { plan, SCRIPTS } from './plan.js';
5
+
6
+ const defaults = { 'lockfile-lint': true };
7
+
8
+ describe('plan', () => {
9
+ test('sets up ESLint, Prettier and lockfile-lint in an empty project', () => {
10
+ assert.deepEqual(plan({}, defaults), {
11
+ packages: ['@amedia/kragl', '@amedia/kragl-lockfile-lint'],
12
+ configs: [
13
+ { tool: 'eslint', filename: 'eslint.config.js' },
14
+ { tool: 'prettier', filename: 'prettier.config.js' },
15
+ ],
16
+ scripts: SCRIPTS,
17
+ });
18
+ });
19
+
20
+ test('sets up Biome without ESLint and Prettier', () => {
21
+ const { packages, configs } = plan({}, { ...defaults, biome: true });
22
+
23
+ assert.deepEqual(packages, [
24
+ '@amedia/kragl-cli',
25
+ '@amedia/kragl-biome',
26
+ '@amedia/kragl-lockfile-lint',
27
+ ]);
28
+ assert.deepEqual(configs, [{ tool: 'biome', filename: 'biome.json' }]);
29
+ });
30
+
31
+ test('stays on Biome when kragl-biome is installed', () => {
32
+ const packageJson = {
33
+ devDependencies: {
34
+ '@amedia/kragl-cli': '1.0.1',
35
+ '@amedia/kragl-biome': '1.3.1',
36
+ },
37
+ };
38
+
39
+ assert.deepEqual(plan(packageJson, defaults), {
40
+ packages: ['@amedia/kragl-lockfile-lint'],
41
+ configs: [{ tool: 'biome', filename: 'biome.json' }],
42
+ scripts: SCRIPTS,
43
+ });
44
+ });
45
+
46
+ test('adds the opt-in runners and leaves out lockfile-lint', () => {
47
+ const { packages } = plan(
48
+ {},
49
+ { 'lockfile-lint': false, 'ls-lint': true, browserslist: true }
50
+ );
51
+
52
+ assert.deepEqual(packages, [
53
+ '@amedia/kragl',
54
+ '@amedia/kragl-ls-lint',
55
+ '@amedia/kragl-browserslist',
56
+ ]);
57
+ });
58
+
59
+ test('skips packages in dependencies or devDependencies', () => {
60
+ const packageJson = {
61
+ dependencies: { '@amedia/kragl': '6.4.0' },
62
+ devDependencies: { '@amedia/kragl-lockfile-lint': '1.0.0' },
63
+ };
64
+
65
+ assert.deepEqual(plan(packageJson, defaults).packages, []);
66
+ });
67
+
68
+ test('keeps existing scripts', () => {
69
+ const packageJson = { scripts: { lint: 'eslint .' } };
70
+
71
+ assert.deepEqual(plan(packageJson, defaults).scripts, {
72
+ format: 'kragl format',
73
+ 'lint:fix': 'kragl lint --fix',
74
+ });
75
+ });
76
+ });
package/src/plan.js ADDED
@@ -0,0 +1,65 @@
1
+ // Runners a project can opt into on top of the base setup, by flag name.
2
+ export const RUNNERS = {
3
+ 'lockfile-lint': '@amedia/kragl-lockfile-lint',
4
+ 'ls-lint': '@amedia/kragl-ls-lint',
5
+ browserslist: '@amedia/kragl-browserslist',
6
+ };
7
+
8
+ // The base setups, and the config file `kragl install --<tool>` writes for
9
+ // each of their tools.
10
+ const BASES = {
11
+ kragl: {
12
+ packages: ['@amedia/kragl'],
13
+ configs: [
14
+ { tool: 'eslint', filename: 'eslint.config.js' },
15
+ { tool: 'prettier', filename: 'prettier.config.js' },
16
+ ],
17
+ },
18
+ biome: {
19
+ packages: ['@amedia/kragl-cli', '@amedia/kragl-biome'],
20
+ configs: [{ tool: 'biome', filename: 'biome.json' }],
21
+ },
22
+ };
23
+
24
+ export const SCRIPTS = {
25
+ format: 'kragl format',
26
+ lint: 'kragl lint',
27
+ 'lint:fix': 'kragl lint --fix',
28
+ };
29
+
30
+ /**
31
+ * Works out what `create-kragl` has to do in a project, leaving out what is
32
+ * already there, so running it again only fills in what is missing.
33
+ *
34
+ * @param {Object} packageJson - The project's package.json.
35
+ * @param {Object} options - The parsed flags: `biome` and one per runner.
36
+ * @returns {{ packages: string[], configs: Object[], scripts: Object }}
37
+ */
38
+ export function plan(packageJson, options) {
39
+ const installed = new Set(
40
+ Object.keys({
41
+ ...packageJson.dependencies,
42
+ ...packageJson.devDependencies,
43
+ })
44
+ );
45
+
46
+ // A project already on Biome stays on Biome without passing --biome again.
47
+ const base =
48
+ options.biome || installed.has('@amedia/kragl-biome')
49
+ ? BASES.biome
50
+ : BASES.kragl;
51
+
52
+ const runners = Object.keys(RUNNERS)
53
+ .filter((name) => options[name])
54
+ .map((name) => RUNNERS[name]);
55
+
56
+ const packages = [...base.packages, ...runners].filter(
57
+ (name) => !installed.has(name)
58
+ );
59
+
60
+ const scripts = Object.fromEntries(
61
+ Object.entries(SCRIPTS).filter(([name]) => !packageJson.scripts?.[name])
62
+ );
63
+
64
+ return { packages, configs: base.configs, scripts };
65
+ }