Skip to content

chore: migrate to the magic tooling stack - #24

Merged
GSTJ merged 15 commits into
masterfrom
chore/magic-tooling
Jul 27, 2026
Merged

chore: migrate to the magic tooling stack#24
GSTJ merged 15 commits into
masterfrom
chore/magic-tooling

Conversation

@GSTJ

@GSTJ GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Moves this package onto the shared GSTJ tooling stack: pnpm, oxlint, oxfmt,
magic-tsconfig, and the reusable CI workflow.

Stack changes

before after
yarn 1 + yarn.lock pnpm 11.17.0 (packageManager) + pnpm-lock.yaml
eslint 8 via expo-module lint (eslint-config-universe) oxlint@1.75.0 + magic-oxlint-config/base
prettier 3 oxfmt@0.60.0 + magic-oxfmt-config
expo-module-scripts/tsconfig.plugin magic-tsconfig/base.json
hand-written lint/test/build job GSTJ/magic/.github/workflows/ci.yml@main

Scripts are the standard set: lint, lint:fix, format, format:fix,
typecheck, plus this repo's build, test, clean and release.

expo-module-scripts is no longer the runner

expo-module build, test, prepare and prepublishOnly all route through a
wrapper that runs yarn exec -- npx … whenever yarn is on PATH and the user
agent is not yarn — exactly the pnpm case, and yarn then refuses on the
packageManager: pnpm@… field. Two more reasons not to keep them:

  • prepare ran expo-module configure, which re-creates the deleted
    .eslintrc.js from a template on every install.
  • prepublishOnly shells out to the proofread bin, a transitive dependency
    that pnpm does not link into node_modules/.bin.

So build is tsc --project tsconfig.build.json and test is jest, which is
what those wrappers were doing. The package stays: expo-module clean and the
jest preset in jest.config.js still come from it.

Notable refactors

  • ResourceXML came from a deep import into @expo/config-plugins, which
    is not a declared dependency — it arrives through expo. Yarn's flat
    node_modules made that resolve; pnpm's does not. It now comes from
    AndroidConfig.Resources on the entry point the file already imported.
  • Kebab-case filenames: 11 renames, 21 import specifiers rewritten, applied
    with magic-kebab --write. No conflicts, nothing skipped, nothing flagged for
    review. git log --follow still reaches back through the renames.
  • tsconfig is split. tsconfig.json (typecheck) covers the tests;
    tsconfig.build.json is the emit config that excludes them. Both re-declare
    CommonJS: Expo requires a config plugin, and the shared base is
    bundler-oriented (module: Preserve), which would emit ESM and break every
    consumer's prebuild. incremental is off for the emit config — its build-info
    file lands outside outDir, so clean && build was emitting nothing at all
    and require("./build") died on the first missing module.
  • .release-it.cjs filters changelog commits with String#startsWith instead
    of a ^-anchored regex.
  • no-template-curly-in-string is off for .release-it.cjs alone — ${version}
    there is release-it's own template syntax and has to reach it uninterpolated.
  • CHANGELOG.md is excluded from oxfmt: @release-it/conventional-changelog
    rewrites it with * bullets on every release, and oxfmt rewrites those to
    -, so the release workflow's own bump PR would fail the format check it just
    created.

CI

checks calls the shared workflow. The second job, validate, exists only to
carry the name "Lint, test and build": master's ruleset requires that exact
status check, and a called workflow reports as <caller job> / <called job>, so
it can never produce that context. Deleting that job is a one-liner once the
ruleset is pointed at the shared workflow's check — a repository settings
change, which this PR deliberately does not make.

release.yml keeps its shape and now installs and runs through pnpm, with
format and typecheck added. Publishing is untouched: release-it still shells
out to npm. CodeQL is untouched.

Verified locally

pnpm install --frozen-lockfile from an empty node_modules, then lint (0
diagnostics), format --check, typecheck, test (5 passing), build, and
npm pack --dry-run (36 files, entry point loads through app.plugin.js).

https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1

GSTJ added 12 commits July 27, 2026 02:37
`@expo/config-plugins` is not a declared dependency — it arrives through
`expo`. Yarn's flat node_modules made the deep import resolve anyway; pnpm's
isolated layout does not, so the build fails with TS2307. The type is
re-exported as `AndroidConfig.Resources.ResourceXML` from the entry point the
rest of the file already imports.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
- pnpm 11.17.0 pinned through `packageManager`; yarn.lock deleted, pnpm-lock
  added. Yarn's `resolutions` become pnpm `overrides` in pnpm-workspace.yaml,
  which is where pnpm 11 reads settings from — the `pnpm` key in package.json
  is ignored now.
- eslint, prettier and the generated `.eslintrc.js` are gone. oxlint 1.75.0 and
  oxfmt 0.60.0 (both pinned exactly) plus magic-oxlint-config,
  magic-oxfmt-config, magic-tsconfig and magic-codemods take their place.
- tsconfig.json extends magic-tsconfig/base.json, re-declaring CommonJS emit:
  Expo `require`s a config plugin, so the bundler-oriented `module: Preserve`
  from the shared base would ship ESM and break every consumer's prebuild.
  tsconfig.build.json is the emit-only variant, so `typecheck` can cover the
  tests without the tests reaching the published tarball.
- `expo-module build`, `expo-module test`, `expo-module prepare` and
  `expo-module prepublishOnly` are replaced by `tsc` and `jest` directly.
  expo-module-scripts@3 routes every one of those through a wrapper that runs
  `yarn exec -- npx …` whenever yarn is on PATH and the user agent is not yarn,
  which is exactly the pnpm case — and yarn then refuses on the
  `packageManager: pnpm@…` field. `prepare` additionally ran `expo-module
  configure`, which re-creates the deleted `.eslintrc.js` from a template on
  every install. `expo-module prepublishOnly` shells out to the `proofread`
  bin, a transitive dependency that pnpm does not link. The jest preset and
  `expo-module clean` still come from the package.
- @types/node and @types/jest are now direct devDependencies; pnpm does not
  expose transitive ones to the type checker.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
`base` is the right variant: this package is plain Node TypeScript that runs
inside Expo's prebuild, with no React or JSX anywhere.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
`unicorn/filename-case` is on in magic-oxlint-config. Applied with
`magic-kebab --write`: 11 renames, 21 import specifiers rewritten, no
conflicts and nothing needing review.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
magic-tsconfig's base turns `incremental` on, and the build info file lands
next to the config rather than inside `outDir`. `pnpm run clean && pnpm run
build` therefore emitted nothing at all — tsc still believed the output it had
just deleted was up to date, and `require("./build")` died on the first missing
module. Off for the emit config; `typecheck` keeps it.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
- `typescript/consistent-type-imports` and `func-style` autofixed (two passes,
  as the magic README warns).
- `.release-it.cjs` filtered changelog commits with `/^chore\(release\)/`;
  `String#startsWith` says the same thing without a regex.
- `no-template-curly-in-string` is off for `.release-it.cjs` alone: `${version}`
  there is release-it's own template syntax and has to reach it uninterpolated,
  so a real template literal would be the bug.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Mostly package.json key sorting, which oxfmt does by default. CHANGELOG.md is
excluded: @release-it/conventional-changelog rewrites it on every release with
`*` bullets, and oxfmt rewrites those to `-`, so the release workflow's own
bump PR would fail the format check it just created.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
- CI calls the shared `GSTJ/magic/.github/workflows/ci.yml`, which installs
  with pnpm and runs lint, format and typecheck; build, test and the
  `npm pack --dry-run` tarball guard stay as this repo's own inputs.
- The `validate` job exists only to carry the name "Lint, test and build".
  master's ruleset requires that exact status check, a called workflow reports
  as "<caller job> / <called job>" and can never produce it, and changing the
  ruleset is a repository setting, not a change this branch can make.
- release.yml installs and runs everything through pnpm, and now checks format
  and typecheck too — it used to lean on `expo-module lint`, which no longer
  exists. Publishing is untouched: release-it still shells out to npm.
- .nvmrc pins node 24, which both workflows read, replacing the version that
  was duplicated in each.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
magic-oxlint-config, magic-oxfmt-config, magic-tsconfig and magic-codemods
all shipped a 1.1.0 that fixes what the 1.0.0 migrations reported.

The pnpm 11 release quarantine still applies — the 1.1.0 tarballs are hours
old — so `minimumReleaseAgeExclude` moves to the new versions rather than
being dropped. Delete those four lines once the packages age past the
three-day window.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
oxfmt: magic-oxfmt-config now ignores `**/CHANGELOG.md` itself, so the
local spread that re-declared it is gone and the config is the plain
re-export the README asks for.

tsconfig: magic-tsconfig no longer sets `incremental`, so `"incremental":
false` in the build config guards against nothing. `*.tsbuildinfo` leaves
.gitignore with it — `tsc --noEmit` and `tsc -p tsconfig.build.json` both
write none now, confirmed after deleting the stale root one.

oxlint: `extends` still drops the preset's `ignorePatterns` on oxlint
1.75.0 with magic-oxlint-config 1.1.0, so the re-declared list could not
just be deleted — checked with `--print-config` and by linting a probe
file under `**/generated/**`, which went from ignored to four diagnostics
the moment the line came out. `extendConfig` flattens instead of
extending, which carries the ignore list by construction and leaves
nothing local to drift.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
magic-oxlint-config 1.1.0 pins `typescript/consistent-type-definitions` to
"type"; 1.0.0 shipped the rule with oxlint's default option, which asks for
the opposite. `--fix` did the rewrite, `oxfmt` added the terminating
semicolon it left off. Nothing imports this as an interface — no
declaration merging, no `implements` — so the alias is a straight swap.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
@GSTJ

GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Updated to the magic 1.1.0 round

Three commits on top of the migration: the bump, the workarounds it retires, and the one code change 1.1.0 forces.

Bumpedmagic-oxlint-config, magic-oxfmt-config, magic-tsconfig 1.0.0 → 1.1.0, magic-codemods 1.0.0 → 1.1.0. No magic-oxlint-plugin here, so 1.0.1 doesn't apply. minimumReleaseAgeExclude moves to the 1.1.0 versions rather than going away — the tarballs are hours old and pnpm 11 enforces the quarantine on --frozen-lockfile. Those four lines are deletable in three days.

Removed

  • oxfmt.config.mts is back to the one-line re-export. 1.1.0 ships **/CHANGELOG.md in the shared ignore list, so the local spread that re-declared it did nothing. oxfmt --check . still leaves CHANGELOG.md alone, which is what keeps release-it's own bump PR from failing the format check.
  • "incremental": false in tsconfig.build.json. magic-tsconfig dropped incremental from base.json, so there was nothing left to switch off. *.tsbuildinfo came out of .gitignore with it; after deleting the stale root tsconfig.tsbuildinfo, neither tsc --noEmit nor tsc -p tsconfig.build.json writes one back. rm -rf build && pnpm run build emits a full tree, twice in a row.

Kept, but rewritten — the ignorePatterns: base.ignorePatterns line. The changelog frames it as the price of the extends form, and it still is: on oxlint 1.75.0 with magic-oxlint-config 1.1.0, extends: [base] resolves to ignorePatterns: []. I checked it two ways rather than trusting --print-config alone — a probe file at src/generated/Probe.ts (matched by **/generated/**, not by .gitignore) is silently skipped with the line and reports four diagnostics without it.

So instead of deleting the line, this config moved to extendConfig(base, …), the mechanism the README documents for repo-specific rules. It flattens rather than extending, so the ignore list arrives by construction and there is no local copy to drift. The one local override — no-template-curly-in-string off for .release-it.cjs, where ${version} is release-it's own template syntax and is supposed to reach it uninterpolated — survives the move and is still load-bearing: 5 diagnostics on that file without it, 0 with.

New violationtypescript/consistent-type-definitions is ["error", "type"] now, where 1.0.0 carried oxlint's default and asked for the opposite. One hit: PluginConfigType in src/plugin-config.ts. --fix converted it, oxfmt added the terminating semicolon the fixer left off. Nothing merges into it or implements it, so the alias is a straight swap and the emitted .d.ts is equivalent.

Verification — from a deleted node_modules: pnpm install --frozen-lockfile, pnpm run build (twice, from an empty build/), pnpm run lint 0 warnings 0 errors across 17 files / 354 rules, pnpm run format clean over 26 files, pnpm run typecheck, pnpm run test 5/5, npm pack --dry-run 36 files.

magic-kebab 1.1.0 --dry-run --strict also re-run against the new detections — 0 renames, 0 conflicts, nothing needing review. Worth doing here specifically because this package is a config plugin and 1.1.0 added the bare-string-literal detection for exactly that shape; app.plugin.js points at ./build, which is emitted and not a rename target.

GSTJ added 2 commits July 27, 2026 06:22
magic-oxlint-config, magic-oxfmt-config and magic-tsconfig only;
magic-codemods stays on 1.1.0, which is still latest.

The pnpm 11 quarantine needs both versions excluded for the one install
that rewrites the lockfile — pnpm verifies the committed lockfile before
it resolves anything, so leaving 1.1.0 out fails on the versions being
left behind. The @1.1.0 lines are gone again after that install;
magic-codemods keeps its own, since 1.1.0 published today too.

Nothing else moved: the lockfile diff is 15 lines each way, all of them
the three specifiers and their resolutions.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Both configs are already the shapes 1.2.0 documents, so this is comments
only. The oxlint one cited `--print-config` as the check that `extends`
drops `ignorePatterns`; 1.2.0 says that command renders an `extends`
config pre-merge and is not a valid audit of one. The reason survives it:
oxlint has no per-override ignore, which is why `extends` is undocumented
now and why `extendConfig` is what this repo uses.

The oxfmt one notes why `withoutIgnorePatterns` does not apply here —
this changelog is generated by @release-it/conventional-changelog, so the
preset's `**/CHANGELOG.md` ignore is the wanted behaviour.

Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
@GSTJ

GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Bumped to magic-oxlint-config / magic-oxfmt-config / magic-tsconfig 1.2.0. magic-codemods stays on 1.1.0 (still latest).

Both quarantine versions had to be listed in minimumReleaseAgeExclude for the install that rewrote the lockfile — pnpm checks the committed lockfile before resolving, so the 1.1.0 entries can only come out afterwards. Final state pins the three at @1.2.0 and keeps magic-codemods@1.1.0, which also published today.

Nothing to unwind from the 1.2.0 fixes: this isn't a Next app, so nextjs.json regaining incremental doesn't reach it (base.json is unchanged and still sets none, and no tsbuildinfo appears from build or typecheck). The CHANGELOG here is generated by @release-it/conventional-changelog, so the preset's **/CHANGELOG.md ignore stays and withoutIgnorePatterns() isn't wanted. oxlint.config.mts was already on extendConfig() rather than defineConfig({ extends }), so the undocumented shape isn't in this repo — verified the flattened config carries the preset's 13 ignorePatterns and its env.

Green: install --frozen-lockfile, build, lint (0), format --check, typecheck, 5/5 tests.

`--report-unused-disable-directives` reports at *warning* severity, and
oxlint exits 0 on warnings, so the one thing that flag exists to catch was
the one thing that couldn't fail CI. Every rule in the shared preset is at
`error`, so this is the only diagnostic `--deny-warnings` changes — verified
by running both forms against a file with a stale directive: exit 0 without
it, exit 1 with it.

This repo just spent a day on a lint step that found things and exited 0
anyway, which is why it gets closed here rather than left for later. The flag
belongs in the shared script in GSTJ/magic for the same reason; until it is
there, this is a one-line local deviation.
@GSTJ
GSTJ merged commit d8f9ec3 into master Jul 27, 2026
4 checks passed
@GSTJ
GSTJ deleted the chore/magic-tooling branch July 27, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant