@webikon/webentor-codemods 0.1.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,24 @@
1
+ # Webentor Codemods Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release of `@webikon/webentor-codemods` — a reusable, ast-grep–powered
6
+ codemod runner for migrating consumer theme code across `webentor-core` updates.
7
+ - CLI: `list`, `run <id>`, and `run --since <ver> [--to <ver>]` (dry-run by
8
+ default, `--apply` to write).
9
+ - Migrations are **named by the webentor-core version** they upgrade to — the id
10
+ *is* the target version, so `run 0.13.0` reads as "run the 0.13.0 codemod".
11
+ - First migration `0.13.0` (core 0.12 → 0.13), with two rule sets:
12
+ - `editor-enqueue-iframe.yml` (PHP): moves the theme's `editor.css` +
13
+ `button.style.css` enqueues from `enqueue_block_editor_assets` to a new
14
+ `enqueue_block_assets` hook (`is_admin()` guarded) for WP 7.0's iframed
15
+ editor; the editor JS stays put.
16
+ - `deps.yml` (JSON, multi-rule): rewrites the theme's `package.json` +
17
+ `composer.json` dependency ranges to the 0.13.0 baseline — `webentor-core`
18
+ `^0.13` plus the full aligned third-party set (incl. major bumps
19
+ `@wordpress/components` 30→35, `@wordpress/icons` 11→14, `stylelint` 16→17,
20
+ `@types/wordpress__block-editor` 14→15). Value-is-string guarded so config
21
+ objects sharing a key aren't touched; `name`/`version` untouched.
22
+ - Polyglot golden tests run a migration's rules over a `before/` fixture subtree
23
+ (PHP + JSON together) and assert byte-exact equality with `after/`, plus
24
+ idempotency and a safe no-op on customized/already-migrated trees.
package/LICENCE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) Webikon s.r.o.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @webikon/webentor-codemods
2
+
3
+ Reusable, polyglot **codemods** for migrating Webentor Stack consumer projects
4
+ across `webentor-core` updates. When a core release requires a mechanical change
5
+ to **consumer-owned theme code** (PHP / TS / CSS / JSON), the fix ships here as a
6
+ versioned migration you can run with one command — no install required.
7
+
8
+ Powered by [ast-grep](https://ast-grep.github.io) (AST-based, so it tolerates
9
+ formatting differences a regex would choke on).
10
+
11
+ ## Usage
12
+
13
+ Run from your **project root** (the directory containing `web/app/themes/…`):
14
+
15
+ ```sh
16
+ # See what's available
17
+ pnpm dlx @webikon/webentor-codemods list
18
+
19
+ # Preview the codemod for a core version (dry-run — prints a diff, writes nothing)
20
+ pnpm dlx @webikon/webentor-codemods run 0.13.0
21
+
22
+ # Apply it
23
+ pnpm dlx @webikon/webentor-codemods run 0.13.0 --apply
24
+
25
+ # Or run every migration across a core version range at once
26
+ pnpm dlx @webikon/webentor-codemods run --since 0.12.0 --to 0.13.0 --apply
27
+ ```
28
+
29
+ `--path <dir>` targets a different directory (defaults to the current one).
30
+ `--package <name>` narrows `--since` to a package (defaults to `webentor-core`).
31
+
32
+ > Use `npx` instead of `pnpm dlx` if you prefer — both download-and-run without
33
+ > a permanent install.
34
+
35
+ Always run a dry-run first, review the diff, then re-run with `--apply`. After
36
+ applying, rebuild the theme and clear caches (`wp acorn optimize:clear`).
37
+
38
+ ## Available migrations
39
+
40
+ Migrations are **named by the webentor-core version** they bring you to — the id
41
+ *is* the target version, so "run the 0.13.0 codemod" reads literally.
42
+
43
+ | id | core | what |
44
+ | --- | --- | --- |
45
+ | `0.13.0` | 0.12 → 0.13 | WP 7.0 iframe editor-asset enqueue change (`app/setup.php`) + the full dependency bump set (`package.json` + `composer.json`) |
46
+
47
+ See each migration's `migrations/<id>/README.md` for before/after detail.
48
+
49
+ ## How it's built
50
+
51
+ ```
52
+ migrations/
53
+ index.json registry (id = version, title, appliesTo range, rule files)
54
+ <version>/ e.g. 0.13.0
55
+ rules/*.yml ast-grep rewrite rule(s), applied in order; one .yml
56
+ may hold several rules as `---`-separated documents
57
+ README.md what/why + before→after
58
+ __fixtures__/<version>/
59
+ before/ after/ golden project subtrees (drive the tests); each holds
60
+ the files a rule touches, e.g. app/setup.php,
61
+ package.json, composer.json
62
+ customized/ (optional) a subtree that must NOT change (no-op guard)
63
+ bin/webentor-codemods.mjs the CLI runner (wraps the bundled ast-grep)
64
+ ```
65
+
66
+ ### Adding a future migration
67
+
68
+ 1. Create `migrations/<version>/rules/<rule>.yml` ast-grep rule(s) with a `fix`.
69
+ Rules can target multiple languages (PHP, JSON, …) — ast-grep dispatches each
70
+ rule to the files matching its `language`. JSON dependency rules use
71
+ `files:` globs to scope to `package.json` / `composer.json`.
72
+ 2. Add `before/` + `after/` (and optional `customized/`) fixture subtrees under
73
+ `migrations/__fixtures__/<version>/`.
74
+ 3. Register it in `migrations/index.json` (`id` = the version, `appliesTo`, `rules`).
75
+
76
+ The runner and the test suite are generic — no code changes needed. Migrations
77
+ are **idempotent** (re-running is a no-op) and **conservative** (anything that
78
+ doesn't match the targeted shape is left untouched).
79
+
80
+ ## Develop & test
81
+
82
+ ```sh
83
+ pnpm install # pulls the ast-grep engine
84
+ pnpm test # golden tests: before→after byte-match, idempotency, no-op safety
85
+ ```
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @webikon/webentor-codemods — reusable codemod runner for Webentor Stack
4
+ * consumer projects.
5
+ *
6
+ * A thin wrapper around the bundled `ast-grep` engine (via the `@ast-grep/cli`
7
+ * dependency). Codemods live in `migrations/<id>/rules/*.yml` and are registered
8
+ * in `migrations/index.json`. The runner is intentionally generic: adding a new
9
+ * migration is just a new folder + an index entry — no runner changes needed.
10
+ *
11
+ * Usage (run from your project root):
12
+ * webentor-codemods list
13
+ * webentor-codemods run <id> [--path <dir>] [--apply]
14
+ * webentor-codemods run --since <ver> [--to <ver>] [--package <name>] [--path <dir>] [--apply]
15
+ *
16
+ * Dry-run by default (prints the diff). Pass --apply to write changes.
17
+ */
18
+ import { createRequire } from 'node:module';
19
+ import { spawnSync } from 'node:child_process';
20
+ import { existsSync, readFileSync } from 'node:fs';
21
+ import { dirname, join, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
26
+ const MIGRATIONS_DIR = join(PKG_ROOT, 'migrations');
27
+
28
+ function fail(msg) {
29
+ console.error(`webentor-codemods: ${msg}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ /** Resolve the bundled ast-grep binary shipped via @ast-grep/cli. */
34
+ function astGrepBin() {
35
+ try {
36
+ const pkgJson = require.resolve('@ast-grep/cli/package.json');
37
+ const bin = join(dirname(pkgJson), 'ast-grep');
38
+ if (existsSync(bin)) return bin;
39
+ } catch {
40
+ /* fall through to PATH */
41
+ }
42
+ return 'ast-grep'; // last resort: rely on PATH
43
+ }
44
+
45
+ function loadRegistry() {
46
+ const indexPath = join(MIGRATIONS_DIR, 'index.json');
47
+ if (!existsSync(indexPath)) fail(`migrations registry not found at ${indexPath}`);
48
+ try {
49
+ return JSON.parse(readFileSync(indexPath, 'utf8'));
50
+ } catch (e) {
51
+ fail(`could not parse migrations/index.json: ${e.message}`);
52
+ }
53
+ }
54
+
55
+ /** Parse a loose "x.y.z" semver into a comparable tuple. Missing parts -> 0. */
56
+ function parseVer(v) {
57
+ const parts = String(v ?? '0')
58
+ .replace(/^[^\d]*/, '')
59
+ .split('.')
60
+ .map((n) => parseInt(n, 10) || 0);
61
+ return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
62
+ }
63
+
64
+ function cmpVer(a, b) {
65
+ const x = parseVer(a);
66
+ const y = parseVer(b);
67
+ for (let i = 0; i < 3; i++) {
68
+ if (x[i] !== y[i]) return x[i] - y[i];
69
+ }
70
+ return 0;
71
+ }
72
+
73
+ function parseArgs(argv) {
74
+ const positional = [];
75
+ const flags = {};
76
+ for (let i = 0; i < argv.length; i++) {
77
+ const a = argv[i];
78
+ if (a.startsWith('--')) {
79
+ const key = a.slice(2);
80
+ const next = argv[i + 1];
81
+ if (next === undefined || next.startsWith('--')) {
82
+ flags[key] = true;
83
+ } else {
84
+ flags[key] = next;
85
+ i++;
86
+ }
87
+ } else {
88
+ positional.push(a);
89
+ }
90
+ }
91
+ return { positional, flags };
92
+ }
93
+
94
+ function printList(registry) {
95
+ const migrations = registry.migrations ?? [];
96
+ if (!migrations.length) {
97
+ console.log('No migrations registered.');
98
+ return;
99
+ }
100
+ console.log('Available migrations:\n');
101
+ for (const m of migrations) {
102
+ const at = m.appliesTo
103
+ ? ` (${m.appliesTo.package} ${m.appliesTo.from} → ${m.appliesTo.to})`
104
+ : '';
105
+ console.log(` ${m.id}${at}`);
106
+ if (m.title) console.log(` ${m.title}`);
107
+ if (m.description) console.log(` ${m.description}`);
108
+ console.log('');
109
+ }
110
+ }
111
+
112
+ /** Run a single migration's rules (in declared order) against targetPath. */
113
+ function runMigration(migration, { targetPath, apply }) {
114
+ const bin = astGrepBin();
115
+ const migDir = join(MIGRATIONS_DIR, migration.id);
116
+ const rules = migration.rules ?? [];
117
+ if (!rules.length) fail(`migration "${migration.id}" declares no rules`);
118
+
119
+ console.log(
120
+ `\n▶ ${migration.id}${apply ? ' (apply)' : ' (dry-run — pass --apply to write)'}`,
121
+ );
122
+ let anyError = false;
123
+ for (const rel of rules) {
124
+ const rulePath = join(migDir, rel);
125
+ if (!existsSync(rulePath)) fail(`rule file not found: ${rulePath}`);
126
+ const args = ['scan', '--rule', rulePath];
127
+ if (apply) args.push('--update-all');
128
+ args.push(targetPath);
129
+ const res = spawnSync(bin, args, { stdio: 'inherit' });
130
+ if (res.error) {
131
+ console.error(` failed to run ast-grep: ${res.error.message}`);
132
+ anyError = true;
133
+ } else if (res.status !== 0) {
134
+ anyError = true;
135
+ }
136
+ }
137
+ return !anyError;
138
+ }
139
+
140
+ function resolveMigrationsToRun(registry, flags, positional) {
141
+ const migrations = registry.migrations ?? [];
142
+
143
+ // `run --since <ver> [--to <ver>] [--package <name>]`
144
+ if (flags.since) {
145
+ const pkg = flags.package || 'webentor-core';
146
+ const from = flags.since;
147
+ const to = flags.to || '999.999.999';
148
+ const selected = migrations.filter((m) => {
149
+ if (!m.appliesTo || m.appliesTo.package !== pkg) return false;
150
+ // Apply migrations whose target ("to") version is in (from, to].
151
+ return cmpVer(m.appliesTo.to, from) > 0 && cmpVer(m.appliesTo.to, to) <= 0;
152
+ });
153
+ if (!selected.length) {
154
+ console.log(
155
+ `No migrations for ${pkg} in range (${from}, ${to}]. Nothing to do.`,
156
+ );
157
+ }
158
+ return selected;
159
+ }
160
+
161
+ // `run <id>`
162
+ const id = positional[0];
163
+ if (!id) fail('run requires a migration <id>, or --since <ver>');
164
+ const found = migrations.find((m) => m.id === id);
165
+ if (!found) fail(`unknown migration "${id}". Run "webentor-codemods list".`);
166
+ return [found];
167
+ }
168
+
169
+ function main() {
170
+ const [, , command, ...rest] = process.argv;
171
+ const { positional, flags } = parseArgs(rest);
172
+
173
+ if (!command || command === 'help' || flags.help) {
174
+ console.log(
175
+ [
176
+ 'webentor-codemods — reusable codemods for Webentor Stack consumer projects',
177
+ '',
178
+ 'Commands:',
179
+ ' list List available migrations',
180
+ ' run <id> [--path <dir>] [--apply] Run one migration (dry-run by default)',
181
+ ' run --since <ver> [--to <ver>] Run every migration in a version range',
182
+ ' [--package <name>] [--path <dir>] [--apply]',
183
+ '',
184
+ 'Run from your project root; --path defaults to the current directory.',
185
+ ].join('\n'),
186
+ );
187
+ process.exit(command ? 0 : 1);
188
+ }
189
+
190
+ const registry = loadRegistry();
191
+
192
+ if (command === 'list') {
193
+ printList(registry);
194
+ return;
195
+ }
196
+
197
+ if (command === 'run') {
198
+ const targetPath = resolve(flags.path || process.cwd());
199
+ if (!existsSync(targetPath)) fail(`--path does not exist: ${targetPath}`);
200
+ const apply = Boolean(flags.apply);
201
+ const toRun = resolveMigrationsToRun(registry, flags, positional);
202
+ let ok = true;
203
+ for (const m of toRun) {
204
+ if (!runMigration(m, { targetPath, apply })) ok = false;
205
+ }
206
+ if (!apply && toRun.length) {
207
+ console.log('\nDry-run complete. Re-run with --apply to write changes.');
208
+ }
209
+ process.exit(ok ? 0 : 1);
210
+ }
211
+
212
+ fail(`unknown command "${command}". Run "webentor-codemods help".`);
213
+ }
214
+
215
+ main();
@@ -0,0 +1,99 @@
1
+ # `0.13.0` — upgrade a consumer theme to webentor-core 0.13.0
2
+
3
+ **Applies to:** consumer themes moving `webentor-core` `0.12.x` → `0.13.0`
4
+ (WordPress 7.0 / PHP 8.4 compatibility pass).
5
+
6
+ Run from your project root:
7
+
8
+ ```sh
9
+ pnpm dlx @webikon/webentor-codemods run 0.13.0 # preview (dry-run)
10
+ pnpm dlx @webikon/webentor-codemods run 0.13.0 --apply # apply
11
+ ```
12
+
13
+ Then reinstall + rebuild + clear caches:
14
+
15
+ ```sh
16
+ # in the theme dir
17
+ pnpm install && composer update
18
+ pnpm build
19
+ wp acorn optimize:clear
20
+ ```
21
+
22
+ This migration has two rule sets: an **editor-asset enqueue** change to
23
+ `app/setup.php`, and a **dependency** bump set across `package.json` +
24
+ `composer.json`.
25
+
26
+ ## 1. Editor-asset enqueue (`rules/editor-enqueue-iframe.yml`, PHP)
27
+
28
+ Since WP 6.9/7.0 the block editor renders inside an iframe. Global canvas styles
29
+ must be registered on `enqueue_block_assets` (not `enqueue_block_editor_assets`)
30
+ so WordPress routes them **into** the iframe — otherwise it warns the style was
31
+ "added to the iframe incorrectly" and the styles never reach the canvas (e.g.
32
+ button styles missing in the editor). The editor **JavaScript** still belongs on
33
+ `enqueue_block_editor_assets` (it loads in the outer editor document).
34
+
35
+ In the theme's `app/setup.php`:
36
+
37
+ ```php
38
+ add_action('enqueue_block_editor_assets', function (): void {
39
+ $dependencies = …;
40
+ \Kucrut\Vite\enqueue_asset(…, 'resources/scripts/editor.ts', …); // JS
41
+ \Kucrut\Vite\enqueue_asset(…, 'resources/styles/editor.css', …); // CSS
42
+ \Kucrut\Vite\enqueue_asset(…, 'resources/core-components/button/button.style.css', …); // CSS
43
+ }, 10);
44
+ ```
45
+
46
+ becomes:
47
+
48
+ ```php
49
+ add_action('enqueue_block_editor_assets', function (): void {
50
+ $dependencies = …;
51
+ \Kucrut\Vite\enqueue_asset(…, 'resources/scripts/editor.ts', …); // JS stays
52
+ }, 10);
53
+
54
+ add_action('enqueue_block_assets', function (): void {
55
+ if (!is_admin()) {
56
+ return;
57
+ }
58
+ \Kucrut\Vite\enqueue_asset(…, 'resources/styles/editor.css', …); // CSS moved
59
+ \Kucrut\Vite\enqueue_asset(…, 'resources/core-components/button/button.style.css', …); // CSS moved
60
+ }, 10);
61
+ ```
62
+
63
+ The `enqueue_asset` directory + options arguments are preserved verbatim (matched
64
+ via metavariables), so per-project tweaks to those options survive.
65
+
66
+ - **Idempotent / conservative:** an already-migrated `setup.php` (no styles left
67
+ on that hook) does not match; a heavily customized hook also won't match and is
68
+ left untouched — migrate it by hand using the before/after above.
69
+
70
+ ## 2. Dependencies (`rules/deps.yml`, JSON)
71
+
72
+ Rewrites the theme's dependency ranges to the **0.13.0 baseline** — the complete
73
+ set that changed from theme 2.0.6 / core 0.12 → 0.13. Each rule matches a
74
+ dependency by its exact key (with a value-is-string guard, so config objects that
75
+ share a name like `lint-staged`/`prettier`/`stylelint` are never touched) and is
76
+ scoped to `package.json` / `composer.json` via `files:`.
77
+
78
+ | manifest | dependency | → range |
79
+ |---|---|---|
80
+ | package.json + composer.json | webentor-core | `^0.13.0` / `^0.13` |
81
+ | package.json | `@wordpress/components` (major 30→35) | `^35.0.1` |
82
+ | package.json | `@wordpress/icons` (major 11→14) | `^14.0.1` |
83
+ | package.json | `stylelint` (major 16→17) | `^17.13.0` |
84
+ | package.json | `stylelint-config-recommended` (major 17→18) | `^18.0.0` |
85
+ | package.json | `@types/wordpress__block-editor` (major 14→15) | `^15.0.6` |
86
+ | package.json | `prettier-plugin-tailwindcss` (0.7→0.8) | `^0.8.0` |
87
+ | package.json | other `@wordpress/*`, `tailwindcss`/`@tailwindcss/*`, `@10up/block-components`, `@alpinejs/intersect`, `@vitejs/plugin-react`, `@roots/vite-plugin`, `vite`, `prettier`, `postcss`, `typescript-eslint`, `eslint`/`@eslint/js`, `eslint-plugin-prettier`, `lint-staged`, `@ianvs/prettier-plugin-sort-imports`, `@shufo/prettier-plugin-blade` | 0.13.0 ranges |
88
+
89
+ **Held back (not changed):** `react`/`react-dom` (18 — WordPress runtime is React
90
+ 18), `eslint`/`@eslint/js` major (10 — blocked by `eslint-plugin-react`), `vite`
91
+ major (8), `typescript` major (6), `lint-staged` major (17 — needs Node 22).
92
+ `name`/`version` are never touched.
93
+
94
+ - **Idempotent:** re-running rewrites to the same range → no diff.
95
+ - **Baseline caveat:** the fix pins the 0.13.0 ranges, so a project already
96
+ *ahead* of the baseline within a major would be set back to it. Expected for a
97
+ 0.12 → 0.13 upgrade; adjust by hand if you intentionally run newer.
98
+
99
+ After applying, run `pnpm install` + `composer update` to refresh the lockfiles.