chore: adopt the shared magic tooling stack - #105
Conversation
Swaps oxlint 1.64 / oxfmt 0.49 for the pinned 1.75.0 / 0.60.0 the shared configs are built against, adds magic-oxlint-config, magic-oxfmt-config, magic-tsconfig and magic-codemods, and moves packageManager to pnpm 11.17.0. pnpm 11 no longer reads the "pnpm" key in package.json, so overrides, patchedDependencies and supportedArchitectures move to pnpm-workspace.yaml together with the two .npmrc settings. It also fails the install outright on unapproved dependency build scripts, hence the allowBuilds block, and refuses to resolve packages published less than 24h ago, hence minimumReleaseAgeExclude for the magic-* releases. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Replaces the hand-maintained .oxlintrc.json / .oxfmtrc.json with the published presets. The two apps get their own config next to their package.json — oxlint resolves the nearest config walking up from each file, and mobile needs `expo` where the site needs `next`. A nested config replaces rather than merges with the root one, so all three restate `ignorePatterns` (`extends` drops them). Only three local overrides survive the dedupe: the gesture-handler scrollable import ban, the database seeds' console output, and the API service layer's static-method classes. tools/tsconfig is gone in favour of magic-tsconfig — base.json and internal-package.json were byte-identical to the published ones. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
`unicorn/filename-case` is on at kebabCase in the shared preset. 96 files
renamed and 357 import specifiers rewritten by `magic-kebab`, run with
apps/mobile/tsconfig.json so the `@/*` aliases were resolved rather than left
pointing at names that no longer exist.
Three things the codemod reports but will not touch, fixed by hand because they
are load-bearing:
- workspace subpath imports (`@pegada/api/services/PaymentService`), which go
through a package `exports` map rather than a relative path;
- the Expo config-plugin paths in app.config.ts, which are plain strings in
the `plugins` array — a miss here fails only on a Linux/EAS build, since
APFS is case-insensitive.
Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
739 errors on first contact, down to zero. The bulk of it:
- 63 inline styles moved into the co-located `styles.ts` as styled components,
plus three shared layout primitives in `@/components/layout` that six form
screens were each spelling out as `style={{ flex: 1 }}`. Style props that
cannot be a component (`contentContainerStyle`, the navigation header slots)
become `StyleSheet.create` entries next to them.
- 23 `async` functions with nothing to await lost the keyword. Two of those
changed real behaviour and were fixed properly rather than reverted:
`customNotificationHandler` is synchronous, so the `.catch(sendError)` on its
call sites never saw the "Invalid notification url" it throws — those are
try/catch now.
- `packages/api`'s services/match/swipe import cycle is broken by moving
`transformDistanceBetweenUserAndDog` to `shared/dog-distance.ts`, where its
three callers can reach it without going through SwipeService.
- `packages/database/__mocks__` is now `fixtures/`: nothing in it is a jest
mock, it is seed data three suites import directly, which is what
`jest/no-mocks-import` was reporting.
- `env.ts` in packages/database and the queue-driver vars in the API config
module, so `process.env` is read in one place per package.
- A real bug in `packages/database/seed.ts`: 100 fake users were created inside
a `forEach` passed to `Promise.all`, so the seed could return before they
were written.
- `LikeLimitReached` renamed to `LikeLimitReachedError`, and every error class
now sets `name` — `instanceof` does not survive tRPC serialization.
Four oxlint autofixes had to be undone or repaired; they are listed in the PR
body. The rest of the disable comments carry a reason each.
Local overrides added, all repo-specific: the gesture-handler scrollable ban,
expo-* and `./styles` namespace imports, the two env boundary modules, the
API's static-method service classes, the single-module error taxonomy,
sequential seeding, `unicorn/no-array-reverse` (Hermes has no `toReversed`),
`react/react-compiler` (bails out on reanimated and react-hook-form) and
`no-warning-comments`.
Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Comments and Maestro flow docs that spell out a source path — the E2E flows name the service they exercise, the widget's Swift target names the sender. The codemod finds these and prints them under NEEDS REVIEW rather than rewriting them, because a string in a YAML file is not an import specifier. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
The lint/format/typecheck matrix in branch-check.yml becomes one call to GSTJ/magic's reusable ci.yml. dedupe and the Postgres-backed test job stay where they are — the first is a lockfile check, the second needs a service container the shared workflow does not provide. release, e2e and deploy are untouched apart from the Node bump. Node has to move: pnpm 11.17 declares `engines.node >= 22.13` and every workflow pinned 20.x, so the runner would refuse to run the package manager this branch pins. .nvmrc goes with it, which is also what the reusable workflow reads. renovate.json now extends the shared preset instead of restating a subset of it; the preset covers what was here plus the oxc and magic-* grouping. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
magic-oxlint-config, magic-oxfmt-config, magic-tsconfig and magic-codemods all go to 1.1.0. magic-oxlint-plugin is not consumed here. minimumReleaseAgeExclude moves with them. pnpm verifies the lockfile against the policy before it resolves anything, so the 1.0.0 entries have to stay in place for the install that rewrites the lockfile and can only come out afterwards — swapping the versions in one edit fails with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION on the entries still in the lockfile. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
magic-tsconfig 1.1.0 takes `incremental` out of base.json, because a stale .tsbuildinfo made `rm -rf dist && tsc` emit nothing and exit 0. Without it `tsBuildInfoFile` is TS5069, a hard error, so every package tsconfig fails typecheck until the option comes out. turbo's `typecheck` task declared that same .tsbuildinfo as its output. It is never written now, which is the "no output files found" warning this repo already removed once for `test`. The task still caches on its inputs. apps/nextjs sets `incremental: false` rather than nothing at all: `next build` writes suggested compiler options back into tsconfig.json when the key is absent, so it re-added `incremental: true` and reformatted the file on every build, leaving a dirty tree and a failing `oxfmt --check`. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
The mobile config turned off `unicorn/no-array-reverse` with the same reasoning the preset now ships (`toReversed()` is ES2023, Hermes is not a safe bet for it). middleware.ts carried a disable for `unicorn/prefer-string-raw`; the `next` preset's App Router override covers that rule now, and --report-unused-disable-directives was reporting the comment. The three configs also move off oxlint's `extends`. It still drops the extended config's `ignorePatterns` on 1.75.0 — checked against 1.1.0 by linting a file under a `generated/` directory with and without the manual re-listing, not by reading --print-config, which under `extends` also omits rule options, categories and jsPlugins that do in fact apply. So the re-listing was load-bearing and easy to get wrong. `extendConfig` flattens instead, and apps/nextjs had nothing but the boilerplate left, so it is the one-line re-export the magic README asks for. Resolved diagnostics over the whole repo are identical before and after. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
…s them typescript/consistent-type-definitions carried oxlint's default option in 1.0.0, which enforces the opposite direction. 1.1.0 sets it to "type", so every interface in the repo was an error. `--fix` did the mechanical part. Four of them keep `interface … extends`, with a disable and a reason. Their base types carry a string index signature — reanimated's Animated.View props, react-native-svg's SvgProps, a styled TextInput — and an intersection distributes that `any` across every member declared next to it, so `startImageIndex?: number` silently became `any`. `extends` keeps the declared type. tsc only caught one of the four (MainCard, via TS7006 on a setState callback that lost its contextual type); the other three were found by asserting `0 extends 1 & T` on each converted property. CustomTypeOptions goes back to an interface as well: it augments i18next's module and a type alias there is TS2300, not a style choice. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Updated to the magic 1.1.0 round
Four commits: the bump, the tsconfig fallout, the lint-config cleanup, the interface → type conversion. What the bump forced
Five of them are back to
Knock-ons: turbo's Redundant local config removed. The mobile config's On the
|
magic-oxlint-config, magic-oxfmt-config and magic-tsconfig only. magic-codemods stays on 1.1.0; it did not ship in this round. The three releases are same-day again, so the pnpm 11 quarantine needs version-pinned `minimumReleaseAgeExclude` entries for them. Swapping the 1.1.0 lines straight to 1.2.0 does not work: pnpm verifies the committed lockfile against the policy before it resolves anything, so the versions being left have to stay excluded for the one install that rewrites it. Both were listed for that install and the 1.1.0 lines are gone now. magic-codemods@1.1.0 keeps its entry, being still in the lockfile and still inside the window. The lockfile diff is 29 lines, all of them the three specifiers and their resolutions. Nothing resolved around the quarantine. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
`incremental: false` in apps/nextjs was there to stop `next build` writing the option into the file itself and reformatting it, which 1.1.0 caused by taking `incremental` out of every base while next kept suggesting it. magic-tsconfig 1.2.0 puts it back in nextjs.json, where it is safe because that base is `noEmit` and next keeps its own build info under .next/cache. Verified rather than assumed: with the override gone, a full `next build` leaves tsconfig.json byte-identical and writes apps/nextjs/tsconfig.tsbuildinfo, which `*.tsbuildinfo` already covers in .gitignore. turbo's `typecheck` task still declares no outputs. A cached `tsc --noEmit` has no artifact worth restoring, and the comment there was claiming nothing gets written at all, which stopped being true for apps/nextjs. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
|
Moved onto the 1.2.0 round: One workaround came out. Nothing to convert for the No pnpm quarantine. Both versions were listed in Checks
|
The push-to-main run for #105 came back `cancelled` with no error in the log. It was not concurrency — cancel-in-progress is scoped to pull_request — and not a lost runner. `Build iOS (required)` simply ran out its `timeout-minutes: 45`, and Actions reports a timed-out job as cancelled. The margin was never there to begin with. The last green run on main took 44m50s for the same job: ten seconds of slack. A cold .app cache makes the job pay for a full Xcode release build (31m) plus the simulator boot, so whether main goes green came down to how fast a given macOS runner felt that morning. The cancel also skipped every cache-save post step, so the next run starts cold again and re-rolls the same dice. Raise both iOS build jobs to 75 minutes. A warm fingerprint hit still finishes in minutes; this only buys the cold path enough room to land. Claude-Session: https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1
Moves pegada onto the shared GSTJ tooling from GSTJ/magic: the same oxlint preset, oxfmt config, tsconfig bases, CI job and Renovate preset every other repo in the set now uses.
Stack
.oxlintrc.jsonmagic-oxlint-config(base/expo/next).oxfmtrc.jsonmagic-oxfmt-configtools/tsconfigpackagemagic-tsconfig(the two files were byte-identical)GSTJ/magic/.github/workflows/ci.yml@maingithub>GSTJ/magicThere was no ESLint or Prettier here to remove — pegada was already on oxlint. What changed is where the config comes from.
Three configs, not one. oxlint resolves the nearest config walking up from each file, and a nested config replaces the root rather than merging with it.
apps/mobilegetsexpo,apps/nextjsgetsnext, everything else getsbasefrom the root. All three restateignorePatterns, becauseextendsdrops them.pnpm 11 moved the settings. The
pnpmkey in package.json is no longer read, sooverrides,patchedDependenciesandsupportedArchitecturesare inpnpm-workspace.yamlnow, along with the two.npmrcsettings. It also hard-fails on unapproved dependency build scripts (allowBuilds) and refuses packages published in the last 24h (minimumReleaseAgeExclude, needed because the magic-* releases are same-day). Node had to go with it: pnpm 11.17 declaresengines.node >= 22.13and every workflow pinned 20.x.Renames
96 files renamed, 357 import specifiers rewritten by
magic-kebab, run with--tsconfig apps/mobile/tsconfig.jsonso the@/*aliases resolved. 0 conflicts, 0 skipped. Rename-only commit, sogit log --followstill works.Two things the codemod reports but deliberately does not touch, fixed by hand in the same commit because the tree is broken without them:
@pegada/api/services/PaymentService), which go through a packageexportsmap rather than a relative path;app.config.ts, which are plain strings in thepluginsarray. A miss there fails only on a Linux/EAS build, since APFS is case-insensitive.Violations
739 errors on first contact, now 0. The ones worth reading:
styles.tsas styled components. Six form screens were each spelling outstyle={{ flex: 1 }}, soFill/Row/KeyboardScreennow live in@/components/layout. Style props that cannot be a component —contentContainerStyle, the react-navigation header slots — becameStyleSheet.createentries beside them.packages/apihad an import cycle between the services, match and swipe modules.transformDistanceBetweenUserAndDogmoved toshared/dog-distance.ts, where its three callers reach it without going throughSwipeService.packages/database/__mocks__is nowfixtures/. Nothing in it was a jest mock; it is seed data three suites import directly, which is exactly whatjest/no-mocks-importwas reporting.packages/database/seed.ts: 100 fake users were created inside aforEachpassed toPromise.all, so nothing awaited them and the seed could return while they were still being written.asyncfunctions with nothing to await lost the keyword. Two changed behaviour and were fixed rather than reverted —customNotificationHandleris synchronous, so the.catch(sendError)at its call sites never saw theInvalid notification urlit throws. Those aretry/catchnow.LikeLimitReached→LikeLimitReachedError, and every class setsname, becauseinstanceofdoes not survive tRPC's serialization boundary but the name does.env.tsinpackages/databaseand the queue-driver vars in the API config module, soprocess.envis read in exactly one place per package.jest.clearAllMocks()deleted from three suites in favour ofclearMocks: truein both runners..jsxfile is now.tsx.Every
eslint-disablebecame anoxlint-disablecarrying a reason, and the 14 that no longer suppressed anything are gone.oxlint
--fixneeded supervisionFour autofixes had to be undone or repaired. Worth knowing before the next repo:
unicorn/prefer-string-rawbroke the Next build. It rewrotemiddleware.ts'smatcherto aString.rawtemplate literal; Next statically analyses that export and fails withInvalid segment configuration export detected. Nothing else catches it — lint, typecheck and tests were all green.unicorn/explicit-length-checkcorrupted logic.data.size ? data.size : nullbecamedata.size > 0 ? …on a field whose type is"SMALL" | "MEDIUM" | "LARGE". The rule assumes any.sizeis a collection.unicorn/catch-error-namerenamed a shorthand property key..catch((cause) => … { cause })became.catch((error) => … { error }), changing the object's key; in another file it renamed a binding onto aconst erroralready declared in the same block.unicorn/no-array-reversetargets ES2023..toReversed()onlib: ES2022and Hermes; both call sites already reversed a copy, so the rule is off for the mobile app — the same call the preset makes forunicorn/no-array-sort.Local overrides
All repo-specific, each carrying its reasoning in the config file: the gesture-handler scrollable import ban,
expo-*and./stylesnamespace imports, the two env boundary modules, the API's static-method service classes, the single-module error taxonomy, sequential seeding,unicorn/no-array-reverse,react/react-compiler(it only reports bailouts on reanimated, gesture-handler and react-hook-form) andno-warning-comments(this repo keeps TODOs as durable notes; the alternatives were deleting them or rewording them to dodge the keyword).Verified
From a clean
node_modules:pnpm install --frozen-lockfile,pnpm dedupe --check, lint 0 diagnostics,format --checkclean, typecheck across all five packages, 73 tests green against a live Postgres, andnext buildproducing all 9 pages. The release, e2e and deploy workflows are untouched apart from the Node bump.https://claude.ai/code/session_01Fe92vvdc4R3F4BDBFoLcw1