neutrinos-cli 2.0.0-beta.5 → 2.0.0-beta.7
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/README.md +20 -2
- package/dist/src/bin/cli.js +9 -1
- package/dist/src/commands/migrate.js +4 -0
- package/dist/src/types/migrate.js +1 -0
- package/dist/src/utils/doctor-checks/cli-update-available.js +30 -0
- package/dist/src/utils/doctor-checks/index.js +4 -0
- package/dist/src/utils/doctor-checks/local-cli-version.js +18 -0
- package/dist/src/utils/migrate-runner.js +115 -0
- package/dist/src/utils/migrations/2.0.0-beta.7.js +76 -0
- package/dist/src/utils/migrations/2.0.0.js +156 -0
- package/dist/src/utils/migrations/index.js +14 -0
- package/dist/src/utils/migrations/version-compare.js +47 -0
- package/package.json +1 -1
- package/templates/component/.component.ts.hbs +2 -2
- package/templates/component/.stories.ts.hbs +6 -0
- package/templates/project/.storybook/main.ts +2 -3
- package/templates/project/tsconfig.json +2 -2
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ neutrinos <command> [options]
|
|
|
41
41
|
| `deprecate [name]` | Deprecate a published package |
|
|
42
42
|
| `auth` | Login or check auth state |
|
|
43
43
|
| `doctor` | Check workspace health (`--fix` to auto-repair) |
|
|
44
|
+
| `migrate` | Migrate workspace to current CLI version (`--dry-run` to preview) |
|
|
44
45
|
| `completion <shell>` | Output shell completion script (`bash` or `zsh`) |
|
|
45
46
|
|
|
46
47
|
### `new <name>`
|
|
@@ -114,20 +115,37 @@ neutrinos doctor # Run all health checks
|
|
|
114
115
|
neutrinos doctor --fix # Auto-fix what it can
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
Runs
|
|
118
|
+
Runs 14 workspace health checks and reports pass/warn/fail for each:
|
|
118
119
|
|
|
119
120
|
- `node_modules` — Dependencies installed
|
|
120
121
|
- `plugin.json` — Exists with required fields (`name`, `components.selectorPrefix`, `modules.idPrefix`)
|
|
121
122
|
- `package.json` — Has `workspaces`, `type: "module"`, expected dependencies
|
|
122
123
|
- `tsconfig.json` — Exists and is valid (verified via `tsc --showConfig`)
|
|
123
124
|
- `plugins-server/` — Directory exists with `index.js`
|
|
125
|
+
- `.storybook/` — Storybook config exists with `main.ts` and `preview.ts`
|
|
126
|
+
- `vitest.config.ts` — Vitest config exists
|
|
124
127
|
- Package `alpha` block — Each package has `alpha.component` or `alpha.module`
|
|
125
128
|
- Component entry file — Component packages have a matching `<name>.ts` file
|
|
126
129
|
- Node version — Meets `>=22` requirement
|
|
127
130
|
- Auth state — Token file exists and is not expired
|
|
128
131
|
- Lock file sync — Lock file is up to date with `package.json`
|
|
132
|
+
- CLI update available — Checks npm registry for newer versions
|
|
133
|
+
- Local CLI version — `devDependencies['neutrinos-cli']` matches running CLI
|
|
129
134
|
|
|
130
|
-
`--fix` auto-repairs: missing `node_modules` (runs install), `plugin.json` defaults, `workspaces`/`type` in `package.json`, `plugins-server/` from template, missing `alpha` blocks, stale lock files.
|
|
135
|
+
`--fix` auto-repairs: missing `node_modules` (runs install), `plugin.json` defaults, `workspaces`/`type` in `package.json`, `plugins-server/` from template, `.storybook/` from template, `vitest.config.ts` from template, missing `alpha` blocks, stale lock files.
|
|
136
|
+
|
|
137
|
+
### `migrate`
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
neutrinos migrate # Migrate workspace to current CLI version
|
|
141
|
+
neutrinos migrate --dry-run # Preview changes without writing
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Upgrades an existing workspace to match the current CLI version. Handles dependency updates, Storybook config scaffolding, file cleanup, and version pinning.
|
|
145
|
+
|
|
146
|
+
The migration enforces git safety: it errors on dirty working trees, auto-inits git if absent, and commits changes on success. All steps are idempotent — safe to re-run after `git checkout .`.
|
|
147
|
+
|
|
148
|
+
Version tracking uses `devDependencies['neutrinos-cli']` — no separate schema version field.
|
|
131
149
|
|
|
132
150
|
### `auth`
|
|
133
151
|
|
package/dist/src/bin/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ import { startPluginsServer } from '../commands/serve.js';
|
|
|
45
45
|
import { runTests } from '../commands/test.js';
|
|
46
46
|
import { completion } from '../commands/completion.js';
|
|
47
47
|
import { doctor } from '../commands/doctor.js';
|
|
48
|
+
import { migrate } from '../commands/migrate.js';
|
|
48
49
|
import { getPackages } from '../utils/get-packages.js';
|
|
49
50
|
import { validateWorkspace } from '../utils/check-valid-ws.js';
|
|
50
51
|
import { done, failed, inprogress, log } from '../utils/logger.js';
|
|
@@ -225,6 +226,13 @@ export const createProgram = () => {
|
|
|
225
226
|
.action((options) => {
|
|
226
227
|
doctor(cwd(), options);
|
|
227
228
|
});
|
|
229
|
+
program
|
|
230
|
+
.command('migrate')
|
|
231
|
+
.description('Migrate workspace to current CLI version')
|
|
232
|
+
.option('--dry-run', 'Preview changes without writing')
|
|
233
|
+
.action((options) => {
|
|
234
|
+
migrate(cwd(), options);
|
|
235
|
+
});
|
|
228
236
|
program
|
|
229
237
|
.command('__list-packages', { hidden: true })
|
|
230
238
|
.description('List workspace package names (used by shell completion)')
|
|
@@ -242,7 +250,7 @@ export const createProgram = () => {
|
|
|
242
250
|
});
|
|
243
251
|
program.hook('preAction', async (_thisCmd, actionCmd) => {
|
|
244
252
|
const cmd = actionCmd.name();
|
|
245
|
-
if (cmd === 'new' || cmd === 'login' || cmd === 'completion' || cmd === '__list-packages' || cmd === 'doctor') {
|
|
253
|
+
if (cmd === 'new' || cmd === 'login' || cmd === 'completion' || cmd === '__list-packages' || cmd === 'doctor' || cmd === 'migrate') {
|
|
246
254
|
return;
|
|
247
255
|
}
|
|
248
256
|
if (!validateWorkspace(cwd())) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { getCliPackagePath } from '../path-utils.js';
|
|
4
|
+
export const cliUpdateAvailableCheck = {
|
|
5
|
+
name: 'CLI update available',
|
|
6
|
+
run() {
|
|
7
|
+
const { version: current } = JSON.parse(readFileSync(getCliPackagePath(), 'utf-8'));
|
|
8
|
+
const isBeta = current.includes('-beta');
|
|
9
|
+
const tag = isBeta ? 'beta' : 'latest';
|
|
10
|
+
try {
|
|
11
|
+
const output = execSync('npm view neutrinos-cli dist-tags --json', {
|
|
12
|
+
stdio: 'pipe',
|
|
13
|
+
encoding: 'utf-8',
|
|
14
|
+
timeout: 3000,
|
|
15
|
+
});
|
|
16
|
+
const tags = JSON.parse(output);
|
|
17
|
+
const latest = tags[tag];
|
|
18
|
+
if (!latest) {
|
|
19
|
+
return [{ name: 'CLI update available', status: 'pass', message: `v${current} (could not determine ${tag} tag)`, fixable: false }];
|
|
20
|
+
}
|
|
21
|
+
if (latest === current) {
|
|
22
|
+
return [{ name: 'CLI update available', status: 'pass', message: `v${current} is the latest ${tag}`, fixable: false }];
|
|
23
|
+
}
|
|
24
|
+
return [{ name: 'CLI update available', status: 'warn', message: `v${current} → v${latest} available. Run "npm i -g neutrinos-cli@${tag}"`, fixable: false }];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [{ name: 'CLI update available', status: 'warn', message: `v${current} (could not reach npm registry)`, fixable: false }];
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -10,6 +10,8 @@ import { componentEntryFileCheck } from './component-entry-file.js';
|
|
|
10
10
|
import { nodeVersionCheck } from './node-version.js';
|
|
11
11
|
import { authStateCheck } from './auth-state.js';
|
|
12
12
|
import { lockFileSyncCheck } from './lock-file-sync.js';
|
|
13
|
+
import { cliUpdateAvailableCheck } from './cli-update-available.js';
|
|
14
|
+
import { localCliVersionCheck } from './local-cli-version.js';
|
|
13
15
|
export const checks = [
|
|
14
16
|
nodeModulesCheck,
|
|
15
17
|
pluginJsonCheck,
|
|
@@ -23,4 +25,6 @@ export const checks = [
|
|
|
23
25
|
nodeVersionCheck,
|
|
24
26
|
authStateCheck,
|
|
25
27
|
lockFileSyncCheck,
|
|
28
|
+
cliUpdateAvailableCheck,
|
|
29
|
+
localCliVersionCheck,
|
|
26
30
|
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getCliPackagePath } from '../path-utils.js';
|
|
4
|
+
export const localCliVersionCheck = {
|
|
5
|
+
name: 'Local CLI version',
|
|
6
|
+
run(wsPath) {
|
|
7
|
+
const installedPkgPath = join(wsPath, 'node_modules', 'neutrinos-cli', 'package.json');
|
|
8
|
+
if (!existsSync(installedPkgPath)) {
|
|
9
|
+
return [{ name: 'Local CLI version', status: 'warn', message: 'neutrinos-cli not installed locally. Run "npm install"', fixable: false }];
|
|
10
|
+
}
|
|
11
|
+
const { version: localVersion } = JSON.parse(readFileSync(installedPkgPath, 'utf-8'));
|
|
12
|
+
const { version: runningVersion } = JSON.parse(readFileSync(getCliPackagePath(), 'utf-8'));
|
|
13
|
+
if (localVersion === runningVersion) {
|
|
14
|
+
return [{ name: 'Local CLI version', status: 'pass', message: `v${localVersion} matches running CLI`, fixable: false }];
|
|
15
|
+
}
|
|
16
|
+
return [{ name: 'Local CLI version', status: 'warn', message: `v${localVersion} differs from running v${runningVersion}. Run "neutrinos migrate"`, fixable: false }];
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { bold, greenBright, red, yellowBright } from 'colorette';
|
|
5
|
+
import { log as _log } from 'node:console';
|
|
6
|
+
import { getCliPackagePath } from './path-utils.js';
|
|
7
|
+
import { migrations } from './migrations/index.js';
|
|
8
|
+
import { compareVersions } from './migrations/version-compare.js';
|
|
9
|
+
import { failed } from './logger.js';
|
|
10
|
+
const SYMBOLS = {
|
|
11
|
+
pass: greenBright('✔'),
|
|
12
|
+
warn: yellowBright('⚠'),
|
|
13
|
+
skip: yellowBright('–'),
|
|
14
|
+
fail: red('✖'),
|
|
15
|
+
};
|
|
16
|
+
const colorize = {
|
|
17
|
+
pass: greenBright,
|
|
18
|
+
warn: yellowBright,
|
|
19
|
+
skip: yellowBright,
|
|
20
|
+
fail: red,
|
|
21
|
+
};
|
|
22
|
+
function printResult(result) {
|
|
23
|
+
const sym = SYMBOLS[result.status];
|
|
24
|
+
const color = colorize[result.status];
|
|
25
|
+
_log(` ${sym} ${color(`${result.name} — ${result.message}`)}`);
|
|
26
|
+
}
|
|
27
|
+
function getWorkspaceCliVersion(wsPath) {
|
|
28
|
+
const installedPkgPath = join(wsPath, 'node_modules', 'neutrinos-cli', 'package.json');
|
|
29
|
+
if (!existsSync(installedPkgPath))
|
|
30
|
+
return null;
|
|
31
|
+
const { version } = JSON.parse(readFileSync(installedPkgPath, 'utf-8'));
|
|
32
|
+
return version;
|
|
33
|
+
}
|
|
34
|
+
function getRunningCliVersion() {
|
|
35
|
+
const { version } = JSON.parse(readFileSync(getCliPackagePath(), 'utf-8'));
|
|
36
|
+
return version;
|
|
37
|
+
}
|
|
38
|
+
function isGitRepo(wsPath) {
|
|
39
|
+
try {
|
|
40
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd: wsPath, stdio: 'pipe' });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function isGitDirty(wsPath) {
|
|
48
|
+
const output = execSync('git status --porcelain', { cwd: wsPath, encoding: 'utf-8' });
|
|
49
|
+
return output.trim().length > 0;
|
|
50
|
+
}
|
|
51
|
+
function gitSnapshot(wsPath) {
|
|
52
|
+
execSync('git init', { cwd: wsPath, stdio: 'pipe' });
|
|
53
|
+
execSync('git add -A', { cwd: wsPath, stdio: 'pipe' });
|
|
54
|
+
execSync('git commit -m "chore: snapshot before migration"', { cwd: wsPath, stdio: 'pipe' });
|
|
55
|
+
}
|
|
56
|
+
function gitCommit(wsPath, version) {
|
|
57
|
+
execSync('git add -A', { cwd: wsPath, stdio: 'pipe' });
|
|
58
|
+
execSync(`git commit -m "chore: migrate workspace to neutrinos-cli@${version}"`, { cwd: wsPath, stdio: 'pipe' });
|
|
59
|
+
}
|
|
60
|
+
export function runMigration(wsPath, opts) {
|
|
61
|
+
const dryRun = opts.dryRun ?? false;
|
|
62
|
+
if (!existsSync(join(wsPath, 'node_modules'))) {
|
|
63
|
+
failed('node_modules not found. Run "npm install" before migrating.');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const cliVersion = getRunningCliVersion();
|
|
67
|
+
const wsVersion = getWorkspaceCliVersion(wsPath);
|
|
68
|
+
if (wsVersion && compareVersions(wsVersion, cliVersion) >= 0) {
|
|
69
|
+
_log(greenBright('\nAlready up to date.\n'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Filter applicable migrations
|
|
73
|
+
const applicable = migrations.filter((m) => {
|
|
74
|
+
if (!wsVersion)
|
|
75
|
+
return true;
|
|
76
|
+
return compareVersions(m.targetVersion, wsVersion) > 0;
|
|
77
|
+
});
|
|
78
|
+
if (applicable.length === 0) {
|
|
79
|
+
_log(greenBright('\nAlready up to date.\n'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Git safety (skip for dry-run)
|
|
83
|
+
if (!dryRun) {
|
|
84
|
+
if (!isGitRepo(wsPath)) {
|
|
85
|
+
_log(yellowBright('\nNo git repository found. Initializing and creating snapshot...\n'));
|
|
86
|
+
gitSnapshot(wsPath);
|
|
87
|
+
}
|
|
88
|
+
else if (isGitDirty(wsPath)) {
|
|
89
|
+
failed('Working tree has uncommitted changes. Commit or stash before migrating.');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const prefix = dryRun ? bold('[dry-run] ') : '';
|
|
94
|
+
for (const migration of applicable) {
|
|
95
|
+
_log(`\n${prefix}Migration: → v${migration.targetVersion}`);
|
|
96
|
+
_log(`${prefix}${migration.description}`);
|
|
97
|
+
_log('──────────────────────────\n');
|
|
98
|
+
const results = migration.steps(wsPath, dryRun);
|
|
99
|
+
let hasFail = false;
|
|
100
|
+
for (const r of results) {
|
|
101
|
+
printResult(r);
|
|
102
|
+
if (r.status === 'fail')
|
|
103
|
+
hasFail = true;
|
|
104
|
+
}
|
|
105
|
+
if (hasFail) {
|
|
106
|
+
_log(red('\n\nMigration failed. Fix the issues above and re-run.\n'));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!dryRun) {
|
|
111
|
+
const lastVersion = applicable[applicable.length - 1].targetVersion;
|
|
112
|
+
gitCommit(wsPath, lastVersion);
|
|
113
|
+
}
|
|
114
|
+
_log(greenBright(`\n${prefix}Migration complete.`) + ' Run "neutrinos doctor" to verify.\n');
|
|
115
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const migration = {
|
|
4
|
+
targetVersion: '2.0.0-beta.7',
|
|
5
|
+
description: 'Bump tsconfig target to ESNext, replace deprecated esbuild with oxc in Storybook config',
|
|
6
|
+
steps(wsPath, dryRun) {
|
|
7
|
+
const results = [];
|
|
8
|
+
// Step 1: Update tsconfig.json target and lib to ESNext
|
|
9
|
+
const tsconfigPath = join(wsPath, 'tsconfig.json');
|
|
10
|
+
if (!existsSync(tsconfigPath)) {
|
|
11
|
+
results.push({ name: 'tsconfig.json', status: 'skip', message: 'File not found' });
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
const raw = readFileSync(tsconfigPath, 'utf-8');
|
|
15
|
+
const tsconfig = JSON.parse(raw);
|
|
16
|
+
const opts = tsconfig.compilerOptions ?? {};
|
|
17
|
+
let changed = false;
|
|
18
|
+
if (opts['target'] !== 'ESNext') {
|
|
19
|
+
opts['target'] = 'ESNext';
|
|
20
|
+
changed = true;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(opts['lib'])) {
|
|
23
|
+
const lib = opts['lib'];
|
|
24
|
+
const updated = lib.map((l) => /^ES\d{4}$/i.test(l) ? 'ESNext' : l);
|
|
25
|
+
if (JSON.stringify(updated) !== JSON.stringify(lib)) {
|
|
26
|
+
opts['lib'] = updated;
|
|
27
|
+
changed = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (changed) {
|
|
31
|
+
tsconfig.compilerOptions = opts;
|
|
32
|
+
if (!dryRun)
|
|
33
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 4) + '\n');
|
|
34
|
+
results.push({ name: 'tsconfig.json', status: 'pass', message: 'Updated target and lib to ESNext' });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
results.push({ name: 'tsconfig.json', status: 'skip', message: 'Already at ESNext' });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Step 2: Replace deprecated esbuild config with build.keepNames in .storybook/main.ts
|
|
41
|
+
const mainTsPath = join(wsPath, '.storybook', 'main.ts');
|
|
42
|
+
if (!existsSync(mainTsPath)) {
|
|
43
|
+
results.push({ name: '.storybook/main.ts', status: 'skip', message: 'File not found' });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
let content = readFileSync(mainTsPath, 'utf-8');
|
|
47
|
+
if (content.includes('esbuild')) {
|
|
48
|
+
// Replace the esbuild-based viteFinal with the oxc-compatible version
|
|
49
|
+
const updatedViteFinal = ` async viteFinal(config) {
|
|
50
|
+
return {
|
|
51
|
+
...config,
|
|
52
|
+
build: {
|
|
53
|
+
...config.build,
|
|
54
|
+
keepNames: true,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},`;
|
|
58
|
+
// Match the viteFinal block regardless of formatting
|
|
59
|
+
const viteFinalPattern = /async\s+viteFinal\s*\(config\)\s*\{[\s\S]*?\n \},/;
|
|
60
|
+
if (viteFinalPattern.test(content)) {
|
|
61
|
+
content = content.replace(viteFinalPattern, updatedViteFinal);
|
|
62
|
+
if (!dryRun)
|
|
63
|
+
writeFileSync(mainTsPath, content);
|
|
64
|
+
results.push({ name: '.storybook/main.ts', status: 'pass', message: 'Replaced deprecated esbuild config with build.keepNames' });
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
results.push({ name: '.storybook/main.ts', status: 'warn', message: 'Contains "esbuild" but viteFinal pattern not recognized. Update manually' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
results.push({ name: '.storybook/main.ts', status: 'skip', message: 'No esbuild config found' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { cpSync, existsSync, globSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { templatesPath } from '../path-utils.js';
|
|
4
|
+
// ── Frozen dependency targets for v2.0.0 ───────────────────────────
|
|
5
|
+
// These are snapshot values — never change them after release.
|
|
6
|
+
// Future dep bumps go in the next migration file.
|
|
7
|
+
const TARGET_DEPS = {
|
|
8
|
+
'express': '^5.2.1',
|
|
9
|
+
'@jatahworx/alpha-annotations-lib': '^1.0.12',
|
|
10
|
+
};
|
|
11
|
+
const TARGET_DEV_DEPS = {
|
|
12
|
+
'neutrinos-cli': '^2.0.0',
|
|
13
|
+
'lit': '^3.3.2',
|
|
14
|
+
'typescript': '^5.9.3',
|
|
15
|
+
'storybook': '^10.3.1',
|
|
16
|
+
'@storybook/web-components-vite': '^10.3.1',
|
|
17
|
+
'@storybook/addon-vitest': '^10.3.1',
|
|
18
|
+
'vitest': '^4.0.0',
|
|
19
|
+
'@vitest/browser': '^4.0.0',
|
|
20
|
+
'@vitest/browser-playwright': '^4.0.0',
|
|
21
|
+
};
|
|
22
|
+
export const migration = {
|
|
23
|
+
targetVersion: '2.0.0',
|
|
24
|
+
description: 'Upgrade workspace from v1.0.x to v2.0.0 (Storybook, updated deps, cleanup)',
|
|
25
|
+
steps(wsPath, dryRun) {
|
|
26
|
+
const results = [];
|
|
27
|
+
// Step 1: Node version check
|
|
28
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
29
|
+
if (major < 22) {
|
|
30
|
+
results.push({ name: 'Node version', status: 'fail', message: `v${process.versions.node} < 22. Install Node.js 22+ before migrating` });
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
results.push({ name: 'Node version', status: 'pass', message: `v${process.versions.node} meets requirement` });
|
|
34
|
+
// Read package.json once
|
|
35
|
+
const pkgPath = join(wsPath, 'package.json');
|
|
36
|
+
if (!existsSync(pkgPath)) {
|
|
37
|
+
results.push({ name: 'Root package.json', status: 'fail', message: 'File is missing — cannot migrate' });
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
41
|
+
// Step 2: type: "module"
|
|
42
|
+
if (pkgJson.type !== 'module') {
|
|
43
|
+
pkgJson.type = 'module';
|
|
44
|
+
results.push({ name: 'Root package.json', status: 'pass', message: 'Added type: "module"' });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
results.push({ name: 'Root package.json', status: 'skip', message: 'type: "module" already set' });
|
|
48
|
+
}
|
|
49
|
+
// Step 3: Remove old npm scripts
|
|
50
|
+
const oldScripts = ['build', 'start', 'serve'];
|
|
51
|
+
const removed = oldScripts.filter((s) => pkgJson.scripts?.[s]);
|
|
52
|
+
if (removed.length > 0) {
|
|
53
|
+
pkgJson.scripts = {};
|
|
54
|
+
results.push({ name: 'npm scripts', status: 'pass', message: `Removed: ${removed.join(', ')}` });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
results.push({ name: 'npm scripts', status: 'skip', message: 'No old scripts to remove' });
|
|
58
|
+
}
|
|
59
|
+
// Steps 4-5: Update dependencies
|
|
60
|
+
pkgJson.dependencies ??= {};
|
|
61
|
+
for (const [dep, version] of Object.entries(TARGET_DEPS)) {
|
|
62
|
+
if (pkgJson.dependencies[dep] && pkgJson.dependencies[dep] !== version) {
|
|
63
|
+
pkgJson.dependencies[dep] = version;
|
|
64
|
+
results.push({ name: `dependency ${dep}`, status: 'pass', message: `Updated to ${version}` });
|
|
65
|
+
}
|
|
66
|
+
else if (!pkgJson.dependencies[dep]) {
|
|
67
|
+
results.push({ name: `dependency ${dep}`, status: 'skip', message: 'Not present in workspace' });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
results.push({ name: `dependency ${dep}`, status: 'skip', message: `Already at ${version}` });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Steps 6-14: Update/add devDependencies
|
|
74
|
+
pkgJson.devDependencies ??= {};
|
|
75
|
+
for (const [dep, version] of Object.entries(TARGET_DEV_DEPS)) {
|
|
76
|
+
const current = pkgJson.devDependencies[dep];
|
|
77
|
+
if (!current) {
|
|
78
|
+
pkgJson.devDependencies[dep] = version;
|
|
79
|
+
results.push({ name: `devDependency ${dep}`, status: 'pass', message: `Added ${version}` });
|
|
80
|
+
}
|
|
81
|
+
else if (current !== version) {
|
|
82
|
+
pkgJson.devDependencies[dep] = version;
|
|
83
|
+
results.push({ name: `devDependency ${dep}`, status: 'pass', message: `Updated ${current} → ${version}` });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
results.push({ name: `devDependency ${dep}`, status: 'skip', message: `Already at ${version}` });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Step 15: Copy .storybook/
|
|
90
|
+
const storybookDir = join(wsPath, '.storybook');
|
|
91
|
+
const storybookTemplate = join(templatesPath(), 'project', '.storybook');
|
|
92
|
+
if (!existsSync(storybookDir)) {
|
|
93
|
+
if (!dryRun)
|
|
94
|
+
cpSync(storybookTemplate, storybookDir, { recursive: true });
|
|
95
|
+
results.push({ name: '.storybook/', status: 'pass', message: 'Copied from template' });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
results.push({ name: '.storybook/', status: 'skip', message: 'Already exists' });
|
|
99
|
+
}
|
|
100
|
+
// Step 16: Copy vitest.config.ts
|
|
101
|
+
const vitestConfig = join(wsPath, 'vitest.config.ts');
|
|
102
|
+
const vitestTemplate = join(templatesPath(), 'project', 'vitest.config.ts');
|
|
103
|
+
if (!existsSync(vitestConfig)) {
|
|
104
|
+
if (!dryRun)
|
|
105
|
+
cpSync(vitestTemplate, vitestConfig);
|
|
106
|
+
results.push({ name: 'vitest.config.ts', status: 'pass', message: 'Copied from template' });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
results.push({ name: 'vitest.config.ts', status: 'skip', message: 'Already exists' });
|
|
110
|
+
}
|
|
111
|
+
// Steps 17-20: Delete obsolete files
|
|
112
|
+
const deletions = [
|
|
113
|
+
['Dockerfile', false],
|
|
114
|
+
['helmchart', true],
|
|
115
|
+
['index.html', false],
|
|
116
|
+
['index.ts', false],
|
|
117
|
+
];
|
|
118
|
+
for (const [name, recursive] of deletions) {
|
|
119
|
+
const path = join(wsPath, name);
|
|
120
|
+
if (existsSync(path)) {
|
|
121
|
+
if (!dryRun)
|
|
122
|
+
rmSync(path, { recursive });
|
|
123
|
+
results.push({ name, status: 'pass', message: 'Removed' });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
results.push({ name, status: 'skip', message: 'Already absent' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Step 21: Scan for plginId typo in module files
|
|
130
|
+
try {
|
|
131
|
+
const moduleFiles = globSync('packages/*/*.js', { cwd: wsPath });
|
|
132
|
+
const affected = [];
|
|
133
|
+
for (const file of moduleFiles) {
|
|
134
|
+
const content = readFileSync(join(wsPath, file), 'utf-8');
|
|
135
|
+
if (content.includes('plginId')) {
|
|
136
|
+
affected.push(file);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (affected.length > 0) {
|
|
140
|
+
results.push({ name: 'plginId typo', status: 'warn', message: `Found in: ${affected.join(', ')}. Rename to "pluginId" manually` });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
results.push({ name: 'plginId typo', status: 'pass', message: 'No occurrences found' });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
results.push({ name: 'plginId typo', status: 'skip', message: 'Could not scan packages' });
|
|
148
|
+
}
|
|
149
|
+
// Step 22: Write package.json (single write)
|
|
150
|
+
if (!dryRun) {
|
|
151
|
+
writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 4) + '\n');
|
|
152
|
+
}
|
|
153
|
+
results.push({ name: 'package.json', status: 'pass', message: 'Written with all updates' });
|
|
154
|
+
return results;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { compareVersions } from './version-compare.js';
|
|
2
|
+
import { migration as v200 } from './2.0.0.js';
|
|
3
|
+
import { migration as v200beta7 } from './2.0.0-beta.7.js';
|
|
4
|
+
// Migration registry — ordered by targetVersion ascending.
|
|
5
|
+
// Add new migrations here. The runner applies them in this order.
|
|
6
|
+
export const migrations = [v200beta7, v200];
|
|
7
|
+
// Validate ordering at import time
|
|
8
|
+
for (let i = 1; i < migrations.length; i++) {
|
|
9
|
+
const prev = migrations[i - 1];
|
|
10
|
+
const curr = migrations[i];
|
|
11
|
+
if (compareVersions(prev.targetVersion, curr.targetVersion) >= 0) {
|
|
12
|
+
throw new Error(`Migration ordering error: ${prev.targetVersion} must come before ${curr.targetVersion}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two semver strings. Returns negative if a < b, 0 if equal, positive if a > b.
|
|
3
|
+
* Handles: major.minor.patch and major.minor.patch-prerelease.N
|
|
4
|
+
* Prerelease versions are less than their release counterpart (2.0.0-beta.3 < 2.0.0).
|
|
5
|
+
*/
|
|
6
|
+
export function compareVersions(a, b) {
|
|
7
|
+
const parse = (v) => {
|
|
8
|
+
const [main, ...preParts] = v.split('-');
|
|
9
|
+
const parts = main.split('.').map(Number);
|
|
10
|
+
const pre = preParts.length > 0 ? preParts.join('-') : null;
|
|
11
|
+
return { parts, pre };
|
|
12
|
+
};
|
|
13
|
+
const va = parse(a);
|
|
14
|
+
const vb = parse(b);
|
|
15
|
+
for (let i = 0; i < 3; i++) {
|
|
16
|
+
const diff = (va.parts[i] ?? 0) - (vb.parts[i] ?? 0);
|
|
17
|
+
if (diff !== 0)
|
|
18
|
+
return diff;
|
|
19
|
+
}
|
|
20
|
+
// Same main version: release > prerelease
|
|
21
|
+
if (!va.pre && !vb.pre)
|
|
22
|
+
return 0;
|
|
23
|
+
if (!va.pre)
|
|
24
|
+
return 1;
|
|
25
|
+
if (!vb.pre)
|
|
26
|
+
return -1;
|
|
27
|
+
// Both have prerelease — compare segments
|
|
28
|
+
const aParts = va.pre.split('.');
|
|
29
|
+
const bParts = vb.pre.split('.');
|
|
30
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
31
|
+
const ap = aParts[i] ?? '';
|
|
32
|
+
const bp = bParts[i] ?? '';
|
|
33
|
+
const an = Number(ap);
|
|
34
|
+
const bn = Number(bp);
|
|
35
|
+
if (!isNaN(an) && !isNaN(bn)) {
|
|
36
|
+
if (an !== bn)
|
|
37
|
+
return an - bn;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
if (ap < bp)
|
|
41
|
+
return -1;
|
|
42
|
+
if (ap > bp)
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -68,9 +68,9 @@ export class {{componentClassName}} extends LitElement {
|
|
|
68
68
|
|
|
69
69
|
render() {
|
|
70
70
|
return html`
|
|
71
|
-
<button @click=${this.increment}>+</button>
|
|
71
|
+
<button ?disabled=${this.disabled} @click=${this.increment}>+</button>
|
|
72
72
|
${this.value || 0}
|
|
73
|
-
<button @click=${this.decrement}>-</button>
|
|
73
|
+
<button ?disabled=${this.disabled} @click=${this.decrement}>-</button>
|
|
74
74
|
`;
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -50,4 +50,10 @@ export const Default: Story = {
|
|
|
50
50
|
|
|
51
51
|
export const Disabled: Story = {
|
|
52
52
|
args: { disabled: true },
|
|
53
|
+
play: async ({ canvasElement }) => {
|
|
54
|
+
const el = canvasElement.querySelector(TAG)!;
|
|
55
|
+
const buttons = el.shadowRoot!.querySelectorAll('button');
|
|
56
|
+
await expect(buttons[0]!).toBeDisabled();
|
|
57
|
+
await expect(buttons[1]!).toBeDisabled();
|
|
58
|
+
},
|
|
53
59
|
};
|