Skip to content

Split babel transformers into separate transform-js and transform-jsx#12

Merged
k0d13 merged 27 commits into
mainfrom
transform
Apr 18, 2026
Merged

Split babel transformers into separate transform-js and transform-jsx#12
k0d13 merged 27 commits into
mainfrom
transform

Conversation

@k0d13
Copy link
Copy Markdown
Owner

@k0d13 k0d13 commented Apr 17, 2026

Closes #9

Summary by CodeRabbit

  • New Features

    • Added JS/TS and JSX/TSX transformer packages and new Next.js & TanStack example apps with i18n wiring.
    • Configuration supports explicit transformer steps alongside formatters.
  • Improvements

    • Simplified plugin and loader architecture; improved build/extract/compile worker orchestration and runtime translation flow.
    • Updated project/tooling TypeScript and workspace layouts.
  • Breaking Changes

    • PO formatter renamed: createFormatter → createPoFormatter.
  • Documentation

    • Updated docs to document transformers and synchronous formatter/transformer APIs.

Greptile Summary

This PR refactors the saykit transformer architecture by splitting the monolithic Babel plugin into two new standalone packages — @saykit/transform-js (for .js/.ts files) and @saykit/transform-jsx (for .jsx/.tsx files) — and introduces a first-class Transformer concept in the config schema alongside the existing Formatter. It also adds new worker classes (BucketBuildWorker, BucketExtractWorker, BucketCompileWorker) for cleaner orchestration of the build/extract/compile pipeline, adds example Next.js and TanStack Start apps, and renames createFormattercreatePoFormatter in the PO package.

Key changes:

  • New Transformer Zod schema in shapes.ts; single transformers are wrapped into an array and composed into a single match/extract/transform interface
  • transform-js and transform-jsx each provide createProgram, extract, and transform using their own internal Babel traversal
  • format-po formatter API changed from async (Promise<>) to synchronous
  • Worker split into BucketExtractWorker, BucketCompileWorker, and BucketBuildWorker
  • All previously flagged critical bugs (reversed extract arguments, broken transform chaining, cooked! null assertion, single-transformer match bypass, stale fallback translations) appear to be resolved in this version

Confidence Score: 4/5

Safe to merge — all previously flagged critical bugs are resolved and no new critical issues were found in this revision.

All prior critical findings (reversed extract args, broken transform chain, null cooked assertion, single-transformer match bypass, wrong fallback locale messages) are addressed in this PR. The transform pipeline correctly uses the accumulator p in the reduce, extract(content, path) arguments are in the correct order, cooked ?? raw fallback is applied, and the Zod composite transformer wrapper guards each individual transformer's match before delegating. The applyFallbackTranslations now correctly reads the fallback locale's catalogue from disk via readCatalogueMessages. No new logic or security bugs were identified. Score is 4 rather than 5 because this is a large multi-package refactor with new example apps and non-trivial AST-level code paths that benefit from integration testing before full confidence.

No files require special attention; all critical paths in packages/config/src/shapes.ts, packages/config/src/features/catalogue/extractor.ts, packages/transform-js/src/, and packages/transform-jsx/src/ look correct.

Reviews (7): Last reviewed commit: "Add labels for transform-js and transfor..." | Re-trigger Greptile

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 17, 2026

🦋 Changeset detected

Latest commit: 9825155

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@saykit/config Patch
@saykit/format-po Patch
saykit Patch
@saykit/carbon Patch
@saykit/react Patch
babel-plugin-saykit Patch
unplugin-saykit Patch
@saykit/transform-jsx Patch
@saykit/transform-js Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
saykit Ready Ready Preview, Comment Apr 18, 2026 8:11am

@github-actions github-actions Bot added dependencies Updates or changes related to project dependencies examples Updates or additions to example apps tests Modifications, additions, or fixes related to testing package: react Related to @saykit/react package: carbon Related to @saykit/carbon package: config Related to @saykit/config and the CLI package: format-po Related to @saykit/format-po package: babel-plugin Related to babel-plugin-saykit package: unplugin Related to unplugin-saykit website Updates to the documentation website labels Apr 17, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR splits Babel transformers into dedicated JS/JSX packages, refactors the config/loader/worker/catalogue/message subsystems (sync config resolution, transformer-aware buckets), updates the Babel/unplugin integration to use transformers, and adds Next.js and TanStack Start examples plus related tooling and docs changes.

Changes

Cohort / File(s) Summary
Monorepo & tooling
package.json, tsconfig.base.json, .changeset/config.json, .changeset/*.md, .oxfmtrc.json
Added website workspace, bumped tool versions (TypeScript → 6.0.3, pnpm), tightened tsconfig for bundler/no-emit, and excluded *.gen.ts from formatter. Changesets updated for new package names.
Config package surface
packages/config/package.json, packages/config/src/index.ts, packages/config/tsdown.config.ts, packages/config/tsconfig.json
Added exports for features, replaced define helper with validate-via-Configuration.parse, and adjusted build entries and tsconfig path alias (~/*).
Configuration schema
packages/config/src/shapes.ts
Introduced synchronous Formatter signatures, new Transformer schema (match/extract/transform), allowed bucket.transformer (single or array), and refined fallbackLocales/buckets typing and validations.
Config loader & resolve
packages/config/src/features/loader/{files,module,resolve,index}.ts
Replaced async useConfig() with sync resolveConfig(). Synchronous file discovery (files.ts). New in-memory module loaders and transpile-and-require flow for JS/TS modules.
Catalogue & message utilities
packages/config/src/features/catalogue/*, packages/config/src/features/messages/*
Added extractor, merging, path expansion, read/write catalogue I/O, message identifier auto-increment (AUTO_INCREMENT_IDENTIFIER), hashing/convert utilities, and a messages barrel export.
Workers refactor
packages/config/src/features/workers/{shared,extract-worker,compile-worker,build-worker,index}.ts
Decomposed monolithic worker into BucketExtractWorker, BucketCompileWorker, BucketBuildWorker and a BucketWorker base; introduced per-file indexing, merge/reconcile flows, compile pipeline and watch behavior.
Runtime translations
packages/config/src/features/runtime/translations.ts
Renamed write function to writeRuntimeTranslations, changed fallback chain construction and fallback message loading to use catalogue read helpers and expandBucketOutputPath.
Commands
packages/config/src/commands/{extract,compile,build}.ts
Commands now call resolveConfig() and import new worker paths; concurrency changed from allSettled to Promise.all (compile/extract/build behavior adjusted).
Transformers (new packages)
packages/transform-js/**, packages/transform-jsx/**
Added @saykit/transform-js and @saykit/transform-jsx packages providing createJsTransformer() and createJsxTransformer() respectively: match/extract/transform implementations, parser/generator changes, tests, build configs and exports.
Babel plugin & unplugin
packages/plugin-babel/*, packages/plugin-unplugin/*
Renamed Babel package to babel-plugin-saykit; simplified plugin to a single Program handler that uses resolveConfig() and bucket.transformer.transform(); unplugin now implemented to apply transformers pre-bundle. Removed older Visitor/Context and several core re-exports.
Examples
examples/nextjs/*, examples/carbon/*, examples/tanstack-start/*
Added Next.js example with .babelrc and typed saykit.config, renamed carbon example, added TanStack Start example (routes, i18n, PO files, Vite config).
Docs & website
website/content/**, website/package.json
Documented transformers, updated installation pages to list new transformer packages and the renamed babel plugin, adjusted wording and removed a feature card; bumped website deps.
Tests & refactors
packages/*/src/*.{test,ts}
Moved/updated tests to match new message model (AUTO_INCREMENT_IDENTIFIER), removed some plugin-babel test suites, and added new message conversion tests in config package.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI/Commands
    participant Config as Config (resolveConfig)
    participant Bucket as BucketWorker
    participant Extract as BucketExtractWorker
    participant Compile as BucketCompileWorker
    participant Transformer as Transformer (JS/JSX)
    participant Formatter as Formatter (PO)
    participant FS as File System

    CLI->>Config: resolveConfig(name)
    Config-->>CLI: Configuration

    CLI->>Bucket: buildAll()/scanAll()/compileAll()
    Bucket->>Extract: scanAll()
    Extract->>FS: glob include patterns
    FS-->>Extract: file list
    loop per file
      Extract->>FS: readFile(path)
      FS-->>Extract: content
      Extract->>Transformer: extract(content, id)
      Transformer-->>Extract: Message[]
    end
    Extract->>Extract: mergeExtractedMessages()
    Extract->>Formatter: stringify(messages, {locale})
    Formatter-->>FS: content
    Extract->>FS: writeCatalogueMessages(path, content)

    Bucket->>Compile: compileAll()
    Compile->>FS: readCatalogueMessages(locale)
    FS-->>Compile: messages
    Compile->>Compile: hydrateTranslations(messages)
    Compile->>FS: writeRuntimeTranslations(locale,json)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~110 minutes

Possibly related PRs

Poem

🐰 I hopped through code, split JS and JSX bright,
Clean buckets, new workers, transformers take flight.
Messages merged, catalogues tidy and neat,
Sync config resolved — the pipeline's complete.
Nibble on changes, then sprint to release tonight! 🥕

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch transform

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/config/src/commands/extract.ts (1)

15-21: ⚠️ Potential issue | 🟡 Minor

Verify Promise.allSettled or add cancellation to prevent partial writes.

Promise.all fails fast on the first bucket rejection, leaving sibling buckets' scanAll/writeAll operations running in the background. More critically, writeAll() writes locale files in a loop—if writeCatalogueMessages fails after writing locales 0–2 of 5, those 2 locales remain on disk and only that bucket's remaining work unwinds, leaving inconsistent output.

Either use Promise.allSettled with aggregated error handling to ensure all buckets complete before handling rejections, or propagate an AbortSignal into workers so all can cancel on first failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/commands/extract.ts` around lines 15 - 21, The current
parallel launch using config.buckets.map and awaiting Promise.all lets one
bucket rejection abort the aggregate while sibling BucketExtractWorker instances
continue running and may partially write files (scanAll / writeAll /
writeCatalogueMessages). Fix by either (A) replacing Promise.all(tasks) with
Promise.allSettled(tasks) and then aggregate and throw a combined error if any
task.status === "rejected", ensuring all buckets finish before error handling,
or (B) add AbortSignal support to BucketExtractWorker (accept a signal in the
constructor and make scanAll/writeAll respect it) then create an
AbortController, pass controller.signal into each new BucketExtractWorker, and
on the first caught rejection call controller.abort() and await
Promise.allSettled to let workers stop cleanly; choose one approach and update
the calling code and BucketExtractWorker methods
(scanAll/writeAll/writeCatalogueMessages) accordingly.
🟡 Minor comments (12)
packages/transform-jsx/src/generator.ts-14-19 (1)

14-19: ⚠️ Potential issue | 🟡 Minor

Add validation for JSX identifier constraints in attribute names.

The empty-string and invalid-character concerns on line 17 are valid edge cases:

  • Object property keys like { "": value } pass an empty string as identifier, which coerces +"" to 0 (not NaN), creating attribute _0 instead of being rejected
  • Object property keys like { "foo-bar": value } or { "foo bar": value } pass invalid JSX identifier characters; t.jsxIdentifier() accepts them at construction time but fails only at code generation/parse time

Whilst JSX attribute names (from getAttributeNameAsString) are constrained by Babel, object property names extracted via getPropertyNameAsString in the upstream parsers are not. Consider validating or sanitising identifiers here to fail fast on invalid input, or confirm that the upstream Message.identifier contract already guarantees valid JavaScript identifiers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/transform-jsx/src/generator.ts` around lines 14 - 19, The attributes
construction currently uses t.jsxIdentifier with strings from id and children
keys without validating JSX identifier rules; update the mapping that builds
attributes (the attributes array and the children.map callback) to validate each
attribute name (including id and each key k) before calling
t.jsxIdentifier—reject or throw a clear error for empty strings or names
containing invalid JSX identifier characters (e.g. spaces, dashes, starting with
digits) so invalid inputs fail fast; alternatively, if sanitisation is chosen,
transform invalid names to a guaranteed-safe form (e.g. prefix with "_" and
replace invalid chars) consistently and document that behavior so
t.jsxAttribute/t.jsxIdentifier never receive invalid names.
packages/config/src/commands/extract.ts-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Remove unnecessary await from synchronous resolveConfig call.

The resolveConfig function in ~/features/loader/resolve.js is synchronous (no async keyword), but line 11 in extract.ts awaits it: const config = await resolveConfig('saykit');. Although awaiting a non-promise value is valid, it's misleading. Remove the await to match the actual function signature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/commands/extract.ts` at line 2, The call to resolveConfig
in extract.ts is using await even though resolveConfig is synchronous; update
the code in the extract command (the line that currently does const config =
await resolveConfig('saykit');) to remove the unnecessary await so it reads
const config = resolveConfig('saykit'); and keep the existing import of
resolveConfig from '~/features/loader/resolve.js'.
website/content/getting-started/installation.mdx-21-26 (1)

21-26: ⚠️ Potential issue | 🟡 Minor

Tautological description for @saykit/babel-plugin.

"Babel plugin for SayKit" restates the package name without adding information. Consider mentioning what it does (e.g. that it delegates to @saykit/transform-js / @saykit/transform-jsx to transform say calls/tagged templates at build time). Same applies to the @saykit/react line above — "i18n hooks and components" is quite vague compared to the previous wording that mentioned <Say> and client/server hooks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/content/getting-started/installation.mdx` around lines 21 - 26,
Update the tautological package descriptions to explain what each package
actually does: change the `@saykit/babel-plugin` line to describe that it
delegates build-time transformation of say calls and tagged templates to
`@saykit/transform-js` and `@saykit/transform-jsx`, and expand the
`@saykit/react` description to mention the provided <Say> component and the
client/server hooks (e.g., useSay/useServerSay) rather than just "i18n hooks and
components" so readers understand the concrete API and build-time
responsibilities.
packages/transform-js/src/parser.test.ts-442-448 (1)

442-448: ⚠️ Potential issue | 🟡 Minor

Weak assertion: toBeInstanceOf(Object) matches virtually anything.

Other tests (Lines 385, 401) correctly assert CompositeMessage. Tighten this one for parity and to actually exercise the type:

✏️ Proposed fix
     expect(result).not.toBeNull();
-    expect(result).toBeInstanceOf(Object); // CompositeMessage
+    expect(result).toBeInstanceOf(CompositeMessage);
     if (result) {
       expect(result.children).toHaveLength(3);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/transform-js/src/parser.test.ts` around lines 442 - 448, The test
currently uses a weak assertion expect(result).toBeInstanceOf(Object); — replace
that with a specific assertion for CompositeMessage so the test actually
verifies the parsed type: assert expect(result).toBeInstanceOf(CompositeMessage)
(keeping the existing checks for children, ArgumentMessage and
AUTO_INCREMENT_IDENTIFIER) so the block that references result,
CompositeMessage, ArgumentMessage and AUTO_INCREMENT_IDENTIFIER exercises the
precise type rather than a generic Object.
examples/tanstack-start/src/routes/__root.tsx-34-34 (1)

34-34: ⚠️ Potential issue | 🟡 Minor

Add a lang attribute to <html>.

Since this example demonstrates SayKit i18n, the root document should set lang on <html> (ideally reflecting the active locale from the route) for accessibility and SEO. A static fallback is better than nothing.

Proposed change
-    <html>
+    <html lang="en">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/tanstack-start/src/routes/__root.tsx` at line 34, The root
document's <html> element is missing a lang attribute; update the render in
routes/__root.tsx to set <html lang={locale || 'en'}> (or similar) so it
reflects the active locale from the route with a static fallback. Retrieve the
active locale from the route loader data or SayKit i18n hook (e.g.,
useLoaderData().locale or your i18n getActiveLocale()/useTranslation() value)
and use that value when rendering the <html> tag in the root document component.
packages/config/src/features/catalogue/extractor.ts-9-9 (1)

9-9: ⚠️ Potential issue | 🟡 Minor

Avoid mutating global Error.stackTraceLimit on every extraction.

Setting Error.stackTraceLimit = Infinity is a process-wide side effect invoked for each file processed. This can bloat memory for unrelated errors thrown elsewhere in the host process (CLI, bundler dev servers) and is also redundant to set per call. If truly needed for transformer debug traces, set it once at module init or guard behind a debug flag; otherwise remove.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/catalogue/extractor.ts` at line 9, Remove the
per-call mutation of Error.stackTraceLimit in the extractor module: do not set
Error.stackTraceLimit = Infinity inside the extraction code path. Either delete
that line entirely or move it to module initialization (top-level once) or guard
it behind a debug flag so it only runs once (e.g., process.env.DEBUG_TRANSFORM).
Target the statement referencing Error.stackTraceLimit in the extractor module
and implement one of these options to avoid a process-wide side effect on every
extraction.
packages/config/src/features/catalogue/extractor.ts-6-7 (1)

6-7: ⚠️ Potential issue | 🟡 Minor

Silently swallowing read errors can mask real failures.

Treating every readFile failure (EACCES, EISDIR, etc.) as "empty file" means extraction will silently skip files the user expected to be processed. Consider logging or only swallowing ENOENT.

Proposed fix
-  const content = await readFile(path, 'utf8').catch(() => '');
-  if (!content) return [];
+  let content: string;
+  try {
+    content = await readFile(path, 'utf8');
+  } catch (err) {
+    if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
+    return [];
+  }
+  if (!content) return [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/catalogue/extractor.ts` around lines 6 - 7, The
current readFile call that assigns to content and swallows all errors (const
content = await readFile(path, 'utf8').catch(() => '')) should be changed to
only treat missing files as empty and surface or log other failures: replace the
blanket .catch with a try/catch (or inspect the caught error) so that if
error.code === 'ENOENT' you return '' and continue, but for other errors
(EACCES, EISDIR, etc.) either rethrow the error or log it via the module's
logger before throwing; update the logic around the content variable and the
early return (if (!content) return []) accordingly so only ENOENT leads to
skipping and other read failures are not silently swallowed.
packages/transform-js/src/generator.ts-14-17 (1)

14-17: ⚠️ Potential issue | 🟡 Minor

Replace t.identifier(ident) with t.stringLiteral(ident) for dynamic property keys.

The ident values from generateChildExpressions include numeric strings (e.g., "0", "1", "2") which are not valid JavaScript identifiers. Using t.identifier() with numeric strings produces invalid Babel AST nodes. Use t.stringLiteral(ident) instead, which correctly handles all string values including numeric ones. The existing test patterns (e.g., t.stringLiteral('1'), t.stringLiteral('one')) confirm this approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/transform-js/src/generator.ts` around lines 14 - 17, The object
property keys created in the properties expression use t.identifier(ident), but
ident values from generateChildExpressions can be numeric strings (e.g., "0",
"1") which produce invalid AST nodes; replace t.identifier(ident) with
t.stringLiteral(ident) in the t.objectProperty calls inside the properties
construction (the const properties = t.objectExpression([...]) that spreads
children.map(([ident, expr]) => t.objectProperty(...))) so dynamic keys are
always emitted as string literals.
packages/config/src/features/loader/resolve.ts-6-12 (1)

6-12: ⚠️ Potential issue | 🟡 Minor

unwrapNamedConfig hardcodes 'saykit' while the caller supplies name.

resolveConfig takes a name parameter and uses it to discover <name>.config.*, but unwrapNamedConfig only unwraps config.saykit. If anyone calls resolveConfig('foo') the named-config branch is silently ignored for config.foo wrappers, which is inconsistent with the file lookup. Either thread name through or document that named wrappers are fixed to saykit.

Proposed fix
-function unwrapNamedConfig(config: object) {
-  if (!('saykit' in config) || !config.saykit) return config;
-  return config.saykit;
-}
+function unwrapNamedConfig(config: object, name: string) {
+  if (!(name in config) || !(config as Record<string, unknown>)[name]) return config;
+  return (config as Record<string, unknown>)[name] as object;
+}
@@
-  config = unwrapNamedConfig(config);
+  config = unwrapNamedConfig(config, name);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/loader/resolve.ts` around lines 6 - 12,
unwrapNamedConfig currently checks for the literal 'saykit' key while
resolveConfig accepts a name parameter, causing wrappers like config.foo to be
ignored; modify unwrapNamedConfig to accept the name (e.g.,
unwrapNamedConfig(config: object, name: string)) or otherwise look up the
dynamic key (config[name]) and return that when present, then update
resolveConfig to call unwrapNamedConfig(fileContent, name) so the unwrapping
matches the discovered config file name.
packages/config/src/features/workers/extract-worker.ts-23-23 (1)

23-23: ⚠️ Potential issue | 🟡 Minor

Minor: log string typo.

Found N messages(s) should be Found N message(s).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/workers/extract-worker.ts` at line 23, Update
the log message in extract-worker.ts where this.logger.step is called with
messages.length and relativePath: change the string from "Found
${messages.length} messages(s) in ${relativePath}" to "Found ${messages.length}
message(s) in ${relativePath)" so the pluralization reads "message(s)"; ensure
the template uses the same variables (messages.length and relativePath) and
preserves surrounding formatting in the this.logger.step call.
examples/nextjs/package.json-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor

Remove or adjust "sideEffects": false from this Next.js example package.

Whilst this is a private example (not published), marking application packages as side-effect-free can interfere with bundler behaviour and isn't recommended for Next.js apps. The codebase already has a side-effect import ('server-only'), which could be problematic if sideEffects is strictly enforced. For an example application, consider removing this flag entirely, or if retained, narrow it to an allow-list (e.g. ["*.css", "*.scss"]).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/nextjs/package.json` at line 5, The package.json currently sets
"sideEffects": false which can break Next.js bundling due to known
side-effectful imports like the 'server-only' module; remove the "sideEffects"
key entirely from this example package.json or replace it with a narrow
allow-list (for example ["*.css","*.scss"]) so that known legitimate side-effect
assets are preserved while avoiding a global false that could tree-shake
required modules.
packages/config/src/shapes.ts-14-23 (1)

14-23: ⚠️ Potential issue | 🟡 Minor

Consider adding format validation to the extension field in the Formatter schema

The extension field now accepts any string via z.string(). However, downstream code in packages/config/src/features/catalogue/path.ts assumes the extension starts with a dot (.slice(1) removes the first character) when constructing file paths. Without validation, invalid inputs like "json", "json.", or an empty string could silently create broken file paths. A minimal regex validator (e.g., /^\.[a-zA-Z0-9]+$/) would catch these typos at config-load time instead of at file I/O.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/shapes.ts` around lines 14 - 23, The Formatter schema's
extension currently accepts any string; update the extension validator in the
Formatter z.object to enforce it starts with a dot and contains alphanumeric
chars (e.g. use z.string().regex(/^\.[a-zA-Z0-9]+$/) with a clear error message)
so downstream code that uses extension.slice(1) (see Formatter type and usage in
catalogue path logic) cannot receive values like "json" or "" that would break
path construction.
🧹 Nitpick comments (22)
packages/config/src/features/messages/identifier.ts (1)

11-25: Readability: add braces to nested if/for blocks

The chained if + body-less if + for without braces is easy to misread (and brittle if a maintainer later adds a statement). Consider using braces and/or an early-return style; behaviour is the same but intent becomes clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/messages/identifier.ts` around lines 11 - 25,
The nested body-less conditionals in assignSequenceIdentifiers make the control
flow hard to read and fragile; update assignSequenceIdentifiers to use explicit
braces (or an early-return pattern) for each if and for block so each
conditional and loop has a clear, block-scoped body—specifically wrap the checks
that use Message instanceof ArgumentMessage | ElementMessage | ChoiceMessage and
the AUTO_INCREMENT_IDENTIFIER assignment, the loop over message.children when
Message instanceof CompositeMessage | ElementMessage, and the loop over
message.branches for ChoiceMessage in explicit { } blocks to preserve existing
behavior while improving readability and safety.
website/content/getting-started/introduction.mdx (1)

29-33: Commented-out JSX left in docs

If the "Framework Agnostic" card is being intentionally dropped, prefer removing the block entirely rather than leaving commented-out JSX in published docs. If it is temporary, a short {/* TODO: ... */} note explaining why/when it'll return would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/content/getting-started/introduction.mdx` around lines 29 - 33,
Remove the commented-out JSX block for the "Framework Agnostic" Card (the Card
component instance and Puzzle icon usage) from the MDX file; if the omission is
temporary, replace the commented JSX with a brief JSX comment like {/* TODO:
re-add Framework Agnostic card when X is implemented */} so maintainers know why
it was removed—locate the block that contains the Card component and the Puzzle
icon and either delete it or swap it for the single-line TODO comment.
packages/plugin-babel/tsdown.config.ts (1)

4-4: Remove redundant entry from glob pattern

src/*.ts already matches src/index.ts, making the explicit entry unnecessary. Simplify to just ['src/*.ts'] to avoid duplication.

Proposed simplification
-  entry: ['src/index.ts', 'src/*.ts'],
+  entry: ['src/*.ts'],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-babel/tsdown.config.ts` at line 4, The entry array in
tsdown.config.ts currently lists both 'src/index.ts' and the glob 'src/*.ts',
causing duplication; update the entry property (entry) to only include the glob
'src/*.ts' so the explicit 'src/index.ts' is removed and the build/input list is
simplified.
packages/transform-jsx/tsconfig.json (1)

1-1: Consider mirroring ~/* alias for consistency.

Sibling packages (packages/config, packages/integration-carbon, packages/integration-react) all add compilerOptions.paths: { "~/*": ["./src/*"] } in this PR. If packages/transform-jsx/src doesn't rely on the alias this is fine, but dropping it asymmetrically is easy to trip over later. Happy to ignore if intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/transform-jsx/tsconfig.json` at line 1, The package's tsconfig.json
currently only extends "../../tsconfig.base.json" and should add the same "~/*"
path alias used by sibling packages; update tsconfig.json to include a
compilerOptions.paths entry: "compilerOptions": { "paths": { "~/*": ["./src/*"]
} } (keeping the existing extends), so imports can use the "~/*" alias
consistently with packages like packages/config and packages/integration-react.
examples/tanstack-start/.gitignore (1)

3-3: Optional: narrow the generated-locale pattern.

src/locales/*/*.json will ignore any JSON placed directly under a locale directory. If hand-written catalogs or schema files ever land there, they'd silently be untracked. If the generator outputs a known filename (e.g. messages.json), pinning the pattern (src/locales/*/messages.json) removes the ambiguity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/tanstack-start/.gitignore` at line 3, Replace the broad
generated-locale ignore pattern so handwritten JSON files aren't unintentionally
ignored: change the pattern "src/locales/*/*.json" to the specific generated
filename (for example "src/locales/*/messages.json") in .gitignore, or add that
specific pattern alongside the existing one if you need to preserve the broader
ignore for other generated files.
packages/config/src/features/messages/hash.test.ts (1)

19-19: Consider using .not.toBe() for clearer intent.

expect(withContext === withoutContext).toBeFalsy() works but obscures the assertion and loses Vitest's diff output on failure. expect(withContext).not.toBe(withoutContext) is more idiomatic and produces better failure messages. Same applies to lines 27–28 (use .toBe(withoutContext)).

♻️ Proposed refactor
-    expect(withContext === withoutContext).toBeFalsy();
+    expect(withContext).not.toBe(withoutContext);
-    expect(withUndefinedContext === withoutContext).toBeTruthy();
-    expect(withEmptyStringContext === withoutContext).toBeTruthy();
+    expect(withUndefinedContext).toBe(withoutContext);
+    expect(withEmptyStringContext).toBe(withoutContext);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/messages/hash.test.ts` at line 19, Replace the
boolean equality assertions that hide Vitest diffs with direct object
comparisons: change expect(withContext === withoutContext).toBeFalsy() to
expect(withContext).not.toBe(withoutContext), and similarly update the other
assertions referenced (the ones that currently use a boolean equality like
expect(... === ...).toBeTruthy()/toBeFalsy()) to use expect(left).toBe(right) or
expect(left).not.toBe(right) as appropriate so Vitest can show proper diff
output; locate these in the test that uses the withContext and withoutContext
variables in hash.test.ts and update those expect calls accordingly.
packages/config/src/commands/build.ts (1)

13-13: Remove redundant await on synchronous resolveConfig() calls.

The resolveConfig() function is synchronous (returns Configuration directly, not a Promise), making the await keywords in build.ts (line 13), compile.ts (line 11), and extract.ts (line 11) redundant. While harmless, they're misleading to readers. Remove the await keywords from these three call sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/commands/build.ts` at line 13, The call sites are using
an unnecessary await on the synchronous resolveConfig() function; remove the
await so you assign the returned Configuration directly (e.g., change "const
config = await resolveConfig('saykit');" to "const config =
resolveConfig('saykit');") in build.ts and do the analogous removal in the
compile.ts and extract.ts calls that invoke resolveConfig(); no other behavior
changes are required.
packages/config/src/features/catalogue/path.ts (1)

7-12: Guard against non-dot-prefixed extensions or document the invariant.

The code at line 11 assumes extension always starts with a dot (e.g. .popo), stripping it with .slice(1). Currently, the sole formatter (@saykit/format-po) does provide .po, but the Formatter schema (shapes.ts:15) allows any string without enforcing the dot prefix. If a future formatter provides po instead, .slice(1) silently produces o with no error.

Either guard the call or document that formatters must provide dot-prefixed extensions:

Optional guard
-  extension = bucket.formatter.extension,
-) {
+  extension = bucket.formatter.extension,
+): string {
+  const ext = extension.startsWith('.') ? extension.slice(1) : extension;
   const outputMessageTemplate = bucket.output
     .replaceAll('{locale}', locale)
-    .replaceAll('{extension}', extension.slice(1));
+    .replaceAll('{extension}', ext);
   return resolve(outputMessageTemplate);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/catalogue/path.ts` around lines 7 - 12, The code
assumes bucket.formatter.extension has a leading dot and uses
extension.slice(1), which breaks for values like "po"; update the replacement to
normalize the extension first: compute a safe value from extension (e.g., if
extension startsWith('.') use extension.slice(1) else use extension unchanged)
and use that safe value when building outputMessageTemplate (referencing
bucket.formatter.extension, the local variable extension, and
outputMessageTemplate/resolve); alternatively add a runtime validation that
throws if extension does not start with '.' so callers must comply—pick one
approach and apply it where the replacement is performed.
examples/tanstack-start/src/i18n.ts (1)

3-9: Refactor locale loading to avoid blocking both locales on module init.

The top-level await import(...) forces both locale JSON files to load synchronously before the module finishes evaluating. In TanStack Start's SSR environment, this blocks the server from rendering the route until both locales resolve—even though the parameterised route (/{-$locale}) only needs the active locale at runtime. Consider lazy-loading the locale catalogs inside the route handler based on the matched locale, rather than awaiting both at module initialisation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/tanstack-start/src/i18n.ts` around lines 3 - 9, The current
top-level creation of the Say instance (const say) uses await import(...) for
both locales which blocks module initialization; change to lazy-load locale
catalogs at runtime instead: remove the top-level await imports from the
messages object in the Say constructor and instead export a loader function
(e.g., loadLocale or getMessages) that dynamically imports the needed
'./locales/{locale}/messages.json' inside your route handler for the matched
locale and then initializes or updates Say with that single locale's messages
(or pass messages into Say.create/instance method) so only the active locale is
loaded during request handling.
examples/nextjs/src/proxy.ts (1)

5-6: Locale list duplicated from saykit.config.ts.

This duplication is flagged by the existing TODO and is fine for an example, but it's worth noting that the two lists must be kept in sync manually. A thin shared module (even within the example) would prevent drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/nextjs/src/proxy.ts` around lines 5 - 6, The SOURCE_LOCALE and
LOCALES arrays are duplicated; create a thin shared module (e.g., export default
or named exports like SOURCE_LOCALE and LOCALES from a new module) that holds
the canonical locale constants, update examples/nextjs/src/proxy.ts to import
SOURCE_LOCALE and LOCALES from that shared module instead of defining them
locally, and update saykit.config.ts to import the same symbols from the shared
module so both files reference the single source of truth (ensure names match
the existing symbols SOURCE_LOCALE and LOCALES).
packages/config/src/features/catalogue/storage.ts (1)

11-12: Catch is broad and may mask real I/O errors.

readFile(...).catch(() => '') treats every failure — permissions, EISDIR, transient errors — as “empty file”, which can silently hide bugs during extract/compile. Consider narrowing to ENOENT:

Proposed refinement
-  const content = await readFile(path, 'utf8').catch(() => '');
-  if (!content) return [];
+  const content = await readFile(path, 'utf8').catch((err) => {
+    if ((err as NodeJS.ErrnoException).code === 'ENOENT') return '';
+    throw err;
+  });
+  if (!content) return [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/catalogue/storage.ts` around lines 11 - 12, The
current readFile call in storage.ts silences all errors by doing readFile(path,
'utf8').catch(() => '') which can hide real I/O problems; change this to catch
only a missing-file error (ENOENT) and rethrow other errors — e.g., wrap the
await readFile(path, 'utf8') in a try/catch, return '' / [] when (err as
NodeJS.ErrnoException).code === 'ENOENT', and rethrow/log otherwise so that
other permission or I/O errors are not masked (refer to the readFile usage, the
content variable, and the subsequent if (!content) return [] logic).
packages/plugin-babel/src/index.ts (1)

6-6: Address the TODO or open a tracking issue.

The transformSource helper is generic enough that unplugin/webpack/vite integrations will want it too — if it stays here it will be duplicated when those adapters land.

Happy to move it into a shared module (e.g. @saykit/config/features/transform) and open an issue to track — want me to draft that?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-babel/src/index.ts` at line 6, The TODO asks to avoid
duplicating the generic transformSource helper; move the transformSource
function out of packages/plugin-babel/src/index.ts into a shared module
(suggested name: `@saykit/config/features/transform`) and update imports in
plugin-babel to import transformSource from that shared module, or if you prefer
not to move now open a short tracking issue referenced in the repo’s issue
tracker that records “Move transformSource to shared feature module” and link it
in this TODO; ensure the exported symbol name remains transformSource so
unplugin/webpack/vite adapters can consume it without changes.
packages/config/src/features/loader/resolve.ts (1)

11-26: Consider memoising the resolved config.

Now that resolveConfig is synchronous and called directly from the Babel plugin’s Program visitor (see packages/plugin-babel/src/index.ts), it will re-run existsSync/readFileSync/safeParse on every transformed file. Since config is effectively immutable during a build, memoising by (name, projectDir) would remove the repeated disk hit without changing semantics for callers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/loader/resolve.ts` around lines 11 - 26,
resolveConfig currently re-reads and re-parses the config on every call; memoise
results by caching per (name, projectDir) to avoid repeated disk/io and parsing.
Add a module-level Map cache keyed by e.g. `${name}::${projectDir}` (use
process.cwd() or accept a second optional projectDir param) and return the
cached parsed result at the top of resolveConfig; only run findConfigFile,
configLoaders load, unwrapNamedConfig and Configuration.safeParse when there is
no cache entry, then store result.data in the cache before returning. Ensure the
cache key references the same project dir used by findConfigFile (process.cwd()
or provided arg) and keep behavior and error throwing unchanged.
packages/plugin-babel/package.json (1)

50-50: Remove the deprecated @types/babel__parser stub package.

@types/babel__parser is a deprecated stub — @babel/parser ships its own TypeScript definitions as of recent versions, making the stub unnecessary. The npm package page for @types/babel__parser explicitly indicates it is no longer needed.

Proposed fix
-  "devDependencies": {
-    "@types/babel__parser": "^7.1.5"
-  }
+  "devDependencies": {}

(or remove the devDependencies block entirely if it becomes empty)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-babel/package.json` at line 50, Remove the deprecated
devDependency entry "@types/babel__parser" from package.json because
`@babel/parser` now ships its own TypeScript types; open
packages/plugin-babel/package.json, delete the "@types/babel__parser" line from
the devDependencies object (or remove the entire devDependencies block if it
becomes empty), and run npm/yarn install to update lockfiles.
packages/config/src/features/loader/module.ts (3)

45-46: js and ts are identical — consider collapsing.

Both loaders delegate to transpileAndRequire with identical behaviour. Either keep two exports for future divergence (document the intent) or export a single transpileAndRequire and reference it directly in configLoaders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/loader/module.ts` around lines 45 - 46, The js
and ts loaders both just call transpileAndRequire; replace the duplicate exports
by exporting transpileAndRequire as the single loader and update any usages
(e.g. in configLoaders) to reference transpileAndRequire directly, or if you
want to keep the two names for future divergence, make js and ts simple aliases
to transpileAndRequire and add a brief comment documenting intent; ensure
symbols js, ts, transpileAndRequire and configLoaders are updated consistently.

40-42: Use rmSync(..., { force: true }) and drop the existence check.

The ts.sys.fileExists check followed by rmSync has a minor TOCTOU window and also requires ts to have been successfully required. Using rmSync(outputPath, { force: true }) is race-free and handles the "file never created" case (e.g. writeFileSync threw) without an existence probe.

♻️ Proposed change
-  } finally {
-    if (ts.sys.fileExists(outputPath)) rmSync(outputPath);
-  }
+  } finally {
+    rmSync(outputPath, { force: true });
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/loader/module.ts` around lines 40 - 42, The
finally block currently checks ts.sys.fileExists(outputPath) before calling
rmSync, which creates a TOCTOU window and depends on ts being available; replace
that pattern by calling rmSync(outputPath, { force: true }) unconditionally to
atomically handle missing files and avoid the existence probe—update the finally
block that references ts.sys.fileExists, rmSync, and outputPath accordingly.

10-10: Write temp file to os.tmpdir() rather than alongside the user's config.

Placing ${path}.${process.pid}.saykit.config.cjs next to the user's source file has two drawbacks:

  1. It dirties the project directory (even transiently) and can trip the recursive watchDebounced('.', …) used by BucketBuildWorker.watch(), producing spurious fs events on every config load.
  2. If the process is killed (SIGKILL) between write and finally, the stale artefact is left in the user's repo.

Prefer path.join(os.tmpdir(), ...) with a unique name (e.g. crypto.randomUUID() plus PID) so temp artefacts are scoped to the OS temp dir and resistant to PID collisions across concurrent saykit invocations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/loader/module.ts` at line 10, The current temp
file name assigned to outputPath (const outputPath =
`${path}.${process.pid}.saykit.config.cjs`) writes next to the user's config;
change it to write into the OS temp directory using path.join(os.tmpdir(), ...)
and a unique filename (e.g. include crypto.randomUUID() and process.pid) to
avoid repo pollution and collisions; update module.ts to import os (and crypto
if not present) and construct outputPath using path.join(os.tmpdir(),
`${crypto.randomUUID()}.${process.pid}.saykit.config.cjs`) while preserving
existing cleanup/finally logic that deletes outputPath.
packages/config/src/features/workers/build-worker.ts (1)

26-40: Consider guarding the watch loop against transient errors.

An exception from extract.updatePath(...) or compile.compileAll() will propagate out of the for await loop and terminate watch mode entirely. For a better developer experience, consider wrapping the body in a try/catch so a single bad save (e.g. syntax error mid-edit) logs and continues rather than killing the watcher.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/workers/build-worker.ts` around lines 26 - 40,
The watch loop currently lets exceptions from extract.updatePath or
compile.compileAll escape and kill the watcher; wrap the loop body inside
watch() (the for await over watchDebounced) in a try/catch so any thrown error
from this.extract.updatePath(...) or this.compile.compileAll() is caught, logged
via this.logger.error with contextual info (include
normalisePathForLogs(filePath) and the error), and then continue to the next
event instead of rethrowing; keep existing logging for successful changes and
only suppress the exception propagation so the watcher stays running.
packages/config/src/features/runtime/translations.ts (1)

14-20: Drop the underscore prefix on bucket — the parameter is actually used.

The _ prefix conventionally signals an unused parameter, but _bucket is forwarded into applyFallbackTranslations on line 20. Consider renaming it back to bucket to match the intent and the downstream parameter name.

♻️ Proposed rename
-  _bucket: Bucket,
+  bucket: Bucket,
   locale: string,
   messages: Message[],
 ) {
   if (cache.has(locale)) return cache.get(locale)!;

-  const translations = await applyFallbackTranslations(cache, config, _bucket, locale, messages);
+  const translations = await applyFallbackTranslations(cache, config, bucket, locale, messages);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/runtime/translations.ts` around lines 14 - 20,
The parameter named `_bucket` is actually used (it's passed to
applyFallbackTranslations), so rename `_bucket` to `bucket` in the function
signature and update all references in that function (including the call to
applyFallbackTranslations) to use `bucket`; ensure any related parameter
destructuring, type annotations (Bucket), and local usages that referenced
`_bucket` are updated to `bucket` so names remain consistent with downstream
code.
packages/transform-js/src/index.ts (1)

40-84: Minor: extract and transform mutate the parsed AST.

On lines 46 and 72, path.node.leadingComments = path.node.leadingComments ?? [] mutates each visited expression node. parseExpression presumably expects a non-undefined leadingComments array; if that's the only reason for this assignment, consider doing the ?? dereference inside parseExpression to keep the AST immutable and make this transformer safely re-usable on an already-parsed program.

Not blocking — just flagging since the AST coming out of createProgram will look subtly different from a vanilla @babel/parser AST.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/transform-js/src/index.ts` around lines 40 - 84, Both extract and
transform currently mutate the AST by assigning path.node.leadingComments =
path.node.leadingComments ?? []; instead move the defensive handling into
parseExpression so callers (extract and transform) no longer change nodes:
revert/remove the assignments in the traverse callbacks in extract and
transform, and update parseExpression to treat undefined leadingComments as an
empty array (e.g., read node.leadingComments ?? [] inside parseExpression) so
the AST returned from createProgram remains unchanged while parseExpression
still works when leadingComments is missing.
packages/transform-js/src/parser.ts (1)

150-156: Drop the misleading as string cast.

getExpressionAsKey returns string | typeof AUTO_INCREMENT_IDENTIFIER, and ArgumentMessage.identifier already accepts that union (per packages/config/src/features/messages/types.ts), so the as string cast on line 152 is both unnecessary and actively lies about the runtime value when the expression is neither an Identifier nor a JSXIdentifier (where key will be AUTO_INCREMENT_IDENTIFIER).

♻️ Proposed cleanup
-  } else if (fallback) {
-    const key = getExpressionAsKey(expression);
-    return new ArgumentMessage(key as string, expression);
-  } else {
+  } else if (fallback) {
+    return new ArgumentMessage(getExpressionAsKey(expression), expression);
+  } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/transform-js/src/parser.ts` around lines 150 - 156, The code casts
the result of getExpressionAsKey(expression) to string before constructing new
ArgumentMessage, which is misleading because getExpressionAsKey returns string |
typeof AUTO_INCREMENT_IDENTIFIER and ArgumentMessage.identifier already accepts
that union; remove the unnecessary "as string" cast and pass key directly to new
ArgumentMessage(key, expression) so the runtime value (including
AUTO_INCREMENT_IDENTIFIER) is preserved; update any affected type annotations if
the TypeScript compiler flags anything, but primarily change the call site in
the block that constructs ArgumentMessage to use the uncast key variable.
packages/config/src/features/messages/types.ts (1)

1-72: Decoupling from @babel/types LGTM; one small refactor worth considering.

Swapping t.Expression for any on expression/accessor is the right call now that transformers live outside @saykit/config, and the AUTO_INCREMENT_IDENTIFIER union is propagated consistently across ArgumentMessage, ElementMessage, ChoiceMessage, and per-branch identifier.

Optional nit: centralising toHashString with this instanceof CompositeMessage (line 11) works but makes Base aware of a subclass. A cleaner pattern would be a protected/virtual getHashContext() method that CompositeMessage overrides to return descriptor.context, letting Base stay agnostic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/features/messages/types.ts` around lines 1 - 72, The Base
class's toHashString currently checks "this instanceof CompositeMessage" making
Base depend on a subclass; refactor by adding a protected method like
getHashContext(): string | undefined on Base (default returns undefined) and
override it in CompositeMessage to return this.descriptor.context, then change
Base.toHashString to call this.getHashContext() instead of doing the instanceof
check; update signatures for Base, CompositeMessage, and toHashString
accordingly (refer to Base.toHashString and CompositeMessage).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40170f56-bb85-443d-a3b9-e6adce95ec6d

📥 Commits

Reviewing files that changed from the base of the PR and between b2f68bc and 812a4d3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (137)
  • .changeset/config.json
  • .changeset/silent-moose-wish.md
  • .oxfmtrc.json
  • examples/carbon/.dev.vars.example
  • examples/carbon/.gitignore
  • examples/carbon/package.json
  • examples/carbon/saykit.config.ts
  • examples/carbon/src/commands/about.ts
  • examples/carbon/src/commands/maths.ts
  • examples/carbon/src/commands/ping.ts
  • examples/carbon/src/commands/roll.ts
  • examples/carbon/src/entry.ts
  • examples/carbon/src/i18n.ts
  • examples/carbon/src/index.ts
  • examples/carbon/src/locales/en/messages.po
  • examples/carbon/src/locales/fr/messages.po
  • examples/carbon/tsconfig.json
  • examples/carbon/tsdown.config.ts
  • examples/carbon/wrangler.toml
  • examples/nextjs-babel/saykit.config.ts
  • examples/nextjs/.babelrc
  • examples/nextjs/.gitignore
  • examples/nextjs/next-env.d.ts
  • examples/nextjs/package.json
  • examples/nextjs/saykit.config.ts
  • examples/nextjs/src/app/[locale]/client-component.tsx
  • examples/nextjs/src/app/[locale]/layout.tsx
  • examples/nextjs/src/app/[locale]/page.tsx
  • examples/nextjs/src/app/[locale]/server-component.tsx
  • examples/nextjs/src/i18n.ts
  • examples/nextjs/src/locales/en/messages.po
  • examples/nextjs/src/locales/fr/messages.po
  • examples/nextjs/src/proxy.ts
  • examples/nextjs/tsconfig.json
  • examples/tanstack-start/.gitignore
  • examples/tanstack-start/package.json
  • examples/tanstack-start/saykit.config.ts
  • examples/tanstack-start/src/i18n.ts
  • examples/tanstack-start/src/locales/en/messages.po
  • examples/tanstack-start/src/locales/fr/messages.po
  • examples/tanstack-start/src/routeTree.gen.ts
  • examples/tanstack-start/src/router.tsx
  • examples/tanstack-start/src/routes/__root.tsx
  • examples/tanstack-start/src/routes/{-$locale}/index.tsx
  • examples/tanstack-start/src/routes/{-$locale}/route.tsx
  • examples/tanstack-start/tsconfig.json
  • examples/tanstack-start/vite.config.ts
  • package.json
  • packages/config/package.json
  • packages/config/src/commands/build.ts
  • packages/config/src/commands/compile.ts
  • packages/config/src/commands/extract.ts
  • packages/config/src/define.ts
  • packages/config/src/features/catalogue/extractor.ts
  • packages/config/src/features/catalogue/merge.ts
  • packages/config/src/features/catalogue/path.ts
  • packages/config/src/features/catalogue/storage.ts
  • packages/config/src/features/extract.ts
  • packages/config/src/features/loader/explorer.ts
  • packages/config/src/features/loader/files.ts
  • packages/config/src/features/loader/index.ts
  • packages/config/src/features/loader/loaders.ts
  • packages/config/src/features/loader/module.ts
  • packages/config/src/features/loader/resolve.ts
  • packages/config/src/features/messages/convert.test.ts
  • packages/config/src/features/messages/convert.ts
  • packages/config/src/features/messages/hash.test.ts
  • packages/config/src/features/messages/hash.ts
  • packages/config/src/features/messages/identifier.ts
  • packages/config/src/features/messages/index.ts
  • packages/config/src/features/messages/types.ts
  • packages/config/src/features/runtime/translations.ts
  • packages/config/src/features/worker.ts
  • packages/config/src/features/workers/build-worker.ts
  • packages/config/src/features/workers/compile-worker.ts
  • packages/config/src/features/workers/extract-worker.ts
  • packages/config/src/features/workers/index.ts
  • packages/config/src/features/workers/shared.ts
  • packages/config/src/index.ts
  • packages/config/src/shapes.ts
  • packages/config/tsconfig.json
  • packages/config/tsdown.config.ts
  • packages/config/vitest.config.ts
  • packages/format-po/src/formatter.ts
  • packages/integration-carbon/package.json
  • packages/integration-carbon/tsconfig.json
  • packages/integration-react/package.json
  • packages/integration-react/src/components/renderer.ts
  • packages/integration-react/tsconfig.json
  • packages/plugin-babel/package.json
  • packages/plugin-babel/src/core/context.ts
  • packages/plugin-babel/src/core/index.ts
  • packages/plugin-babel/src/core/messages/convert.test.ts
  • packages/plugin-babel/src/core/program.test.ts
  • packages/plugin-babel/src/core/program.ts
  • packages/plugin-babel/src/core/visitor.test.ts
  • packages/plugin-babel/src/core/visitor.ts
  • packages/plugin-babel/src/features/jsx/generator.ts
  • packages/plugin-babel/src/index.ts
  • packages/plugin-babel/src/play.ts
  • packages/plugin-babel/tsdown.config.ts
  • packages/plugin-unplugin/package.json
  • packages/plugin-unplugin/src/bun.ts
  • packages/plugin-unplugin/src/esbuild.ts
  • packages/plugin-unplugin/src/farm.ts
  • packages/plugin-unplugin/src/index.ts
  • packages/plugin-unplugin/src/rolldown.ts
  • packages/plugin-unplugin/src/rollup.ts
  • packages/plugin-unplugin/src/rspack.ts
  • packages/plugin-unplugin/src/unloader.ts
  • packages/plugin-unplugin/src/vite.ts
  • packages/plugin-unplugin/src/webpack.ts
  • packages/plugin-unplugin/tsconfig.json
  • packages/plugin-unplugin/tsdown.config.ts
  • packages/transform-js/package.json
  • packages/transform-js/src/generator.ts
  • packages/transform-js/src/index.ts
  • packages/transform-js/src/parser.test.ts
  • packages/transform-js/src/parser.ts
  • packages/transform-js/tsconfig.json
  • packages/transform-js/tsdown.config.ts
  • packages/transform-js/vitest.config.ts
  • packages/transform-jsx/package.json
  • packages/transform-jsx/src/generator.ts
  • packages/transform-jsx/src/index.ts
  • packages/transform-jsx/src/parser.test.ts
  • packages/transform-jsx/src/parser.ts
  • packages/transform-jsx/tsconfig.json
  • packages/transform-jsx/tsdown.config.ts
  • packages/transform-jsx/vitest.config.ts
  • packages/unplugin/src/index.ts
  • tsconfig.base.json
  • website/content/core-concepts/configuration.mdx
  • website/content/core-concepts/runtime.mdx
  • website/content/getting-started/installation.mdx
  • website/content/getting-started/introduction.mdx
  • website/package.json
💤 Files with no reviewable changes (16)
  • packages/config/vitest.config.ts
  • packages/config/src/define.ts
  • packages/unplugin/src/index.ts
  • packages/plugin-babel/src/features/jsx/generator.ts
  • packages/config/src/features/loader/explorer.ts
  • packages/plugin-babel/src/core/index.ts
  • packages/plugin-babel/src/core/messages/convert.test.ts
  • packages/plugin-babel/src/core/context.ts
  • packages/plugin-babel/src/core/program.test.ts
  • packages/config/src/features/extract.ts
  • packages/plugin-babel/src/core/visitor.test.ts
  • packages/plugin-babel/src/core/program.ts
  • packages/plugin-babel/src/core/visitor.ts
  • examples/nextjs-babel/saykit.config.ts
  • packages/config/src/features/loader/loaders.ts
  • packages/config/src/features/worker.ts

Comment thread examples/nextjs/package.json
Comment thread examples/tanstack-start/vite.config.ts Outdated
Comment thread packages/config/package.json
Comment thread packages/config/src/features/catalogue/merge.ts
Comment thread packages/config/src/features/messages/identifier.ts
Comment thread packages/plugin-babel/src/play.ts Outdated
Comment thread packages/plugin-unplugin/src/index.ts Outdated
Comment thread packages/transform-jsx/src/index.ts
Comment thread packages/transform-jsx/src/parser.ts Outdated
Comment thread website/package.json
@k0d13
Copy link
Copy Markdown
Owner Author

k0d13 commented Apr 17, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/plugin-babel/src/index.ts (1)

21-25: ⚠️ Potential issue | 🔴 Critical

Still unresolved: path.replaceWith(ast.program) on Program is unsupported and will throw.

This mirrors the earlier critical review comment, which does not appear to have been addressed. @babel/traverse does not support replacing a Program path with another Program node — the root has no parentPath, and replaceWith internally delegates to operations that require one. See babel/babel#1436. Mutate path.node.body / path.node.directives directly instead:

🐛 Proposed fix
         if (transformed !== code) {
           const ast = parse(transformed, state.file.opts.parserOpts as any);
-          path.replaceWith(ast.program);
-          path.skip();
+          path.node.body = ast.program.body;
+          path.node.directives = ast.program.directives;
+          path.skip();
         }

Secondary concerns that remain from the earlier review:

  1. state.file.opts.parserOpts is @babel/core's shape (ParserOptions merged with plugin arrays as strings/tuples), not @babel/parser's public ParserOptions. The as any hides a real type mismatch, and if the original Babel config didn't enable the JSX/TS plugin that the transformer emitted, parse() will throw mid-build.
  2. Transforming state.file.code (the pristine source) discards any AST edits made by earlier plugins in the pipeline. If downstream users combine @saykit/plugin-babel with other Babel plugins, those edits are silently lost. Either generate source from path first, or document/enforce this plugin's ordering requirement.
`@babel/traverse` NodePath replaceWith Program root node supported 2026
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-babel/src/index.ts` around lines 21 - 25, Replace the
unsupported Program-level replaceWith: instead of calling
path.replaceWith(ast.program), mutate the existing Program node by assigning
path.node.body = ast.program.body and path.node.directives =
ast.program.directives (and copy any other top-level properties you need) so the
root keeps its parent context; when re-parsing, avoid using state.file.code
(which discards prior plugin edits) — generate source from the current path
(e.g., use `@babel/generator` to get generated = generate(path.node).code) and
parse that generated code with proper parser options (do not use
state.file.opts.parserOpts as any; ensure the parser plugins like jsx/ts are
enabled or map the `@babel/core` ParserOptions to `@babel/parser` options) before
applying the body/directives swap.
🧹 Nitpick comments (2)
packages/plugin-babel/src/index.ts (2)

6-7: Consider resolving config lazily to avoid import-time throws.

resolveConfig() throws when the config file is missing or invalid. Calling it at module scope means any consumer that imports @saykit/plugin-babel (IDE tooling, test harnesses, downstream plugins) will throw before the plugin factory is ever invoked — which is significantly more hostile than throwing on first transform. Also, Babel’s cwd option can differ from process.cwd(), yet resolveConfig() is evaluated before the factory receives any Babel API.

Consider resolving inside the factory (or memoising on first Program visit) so the throw surfaces through Babel’s plugin-loading error channel and honours Babel’s configured cwd:

♻️ Proposed refactor
-export default function (): PluginObj {
-  const config = resolveConfig();
-
-  return {
+export default function (): PluginObj {
+  let config: ReturnType<typeof resolveConfig> | undefined;
+  return {
     name: 'saykit',
     visitor: {
       Program(path, state) {
+        config ??= resolveConfig();
         const id_ = state.file.opts.filename;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-babel/src/index.ts` around lines 6 - 7, The module currently
calls resolveConfig() at import time inside the default exported function
signature, which can throw before Babel invokes the plugin and ignores Babel's
cwd; move the call to resolveConfig() out of module scope into the plugin
factory so it runs lazily (either at the start of the exported default function
body or deferred until the first Program visitor invocation) and memoize the
resolved value so subsequent transforms reuse it; ensure the code uses the Babel
API/options (e.g., the received cwd or plugin options) when calling
resolveConfig() so errors surface through Babel's plugin-loading/transform
channel rather than at import time.

14-14: Nit: node_modules substring check can over-match.

id_.includes('node_modules') will skip any absolute path that happens to contain the substring anywhere (e.g., /home/user/my-node_modules_backup/..., or a monorepo package path with node_modules in its name). Matching path segments is safer:

-        if (!id_ || id_.includes('node_modules')) return;
+        if (!id_ || /[\\/]node_modules[\\/]/.test(id_)) return;

Also worth aligning with @saykit/plugin-unplugin if it uses the same guard, so the two plugins behave identically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-babel/src/index.ts` at line 14, The current guard uses
id_.includes('node_modules') which can false-positive on paths that merely
contain that substring; change the check in the file's loader (the branch where
id_ is checked) to match node_modules as a path segment instead (e.g., use a
path-aware test such as splitting id_ by path separators or using a regex that
checks for node_modules as a segment like (^|[/\\])node_modules([/\\]|$));
update the conditional that currently reads "if (!id_ ||
id_.includes('node_modules')) return;" to use this safer path-segment check for
id_, and make the behavior consistent with the guard used in
`@saykit/plugin-unplugin`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/config/src/shapes.ts`:
- Line 15: The extension field in shapes.ts was loosened to z.string(), which
allows values without a leading dot and empty strings causing double dots in
output ('.{extension}'); update the validation for the extension property to
require a leading dot and at least one character after it (or alternatively
enforce min length > 0 and forbid a leading dot depending on intended semantics)
by replacing the current z.string() with a stricter validator (e.g., a
regex-based z.string().regex(...) or a template-literal style validator) so that
the extension always yields a single leading dot when interpolated into output
paths.

---

Duplicate comments:
In `@packages/plugin-babel/src/index.ts`:
- Around line 21-25: Replace the unsupported Program-level replaceWith: instead
of calling path.replaceWith(ast.program), mutate the existing Program node by
assigning path.node.body = ast.program.body and path.node.directives =
ast.program.directives (and copy any other top-level properties you need) so the
root keeps its parent context; when re-parsing, avoid using state.file.code
(which discards prior plugin edits) — generate source from the current path
(e.g., use `@babel/generator` to get generated = generate(path.node).code) and
parse that generated code with proper parser options (do not use
state.file.opts.parserOpts as any; ensure the parser plugins like jsx/ts are
enabled or map the `@babel/core` ParserOptions to `@babel/parser` options) before
applying the body/directives swap.

---

Nitpick comments:
In `@packages/plugin-babel/src/index.ts`:
- Around line 6-7: The module currently calls resolveConfig() at import time
inside the default exported function signature, which can throw before Babel
invokes the plugin and ignores Babel's cwd; move the call to resolveConfig() out
of module scope into the plugin factory so it runs lazily (either at the start
of the exported default function body or deferred until the first Program
visitor invocation) and memoize the resolved value so subsequent transforms
reuse it; ensure the code uses the Babel API/options (e.g., the received cwd or
plugin options) when calling resolveConfig() so errors surface through Babel's
plugin-loading/transform channel rather than at import time.
- Line 14: The current guard uses id_.includes('node_modules') which can
false-positive on paths that merely contain that substring; change the check in
the file's loader (the branch where id_ is checked) to match node_modules as a
path segment instead (e.g., use a path-aware test such as splitting id_ by path
separators or using a regex that checks for node_modules as a segment like
(^|[/\\])node_modules([/\\]|$)); update the conditional that currently reads "if
(!id_ || id_.includes('node_modules')) return;" to use this safer path-segment
check for id_, and make the behavior consistent with the guard used in
`@saykit/plugin-unplugin`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 700eeebb-555e-4d08-82f8-b7530dc8c28a

📥 Commits

Reviewing files that changed from the base of the PR and between 812a4d3 and 9a7c99f.

📒 Files selected for processing (5)
  • examples/tanstack-start/vite.config.ts
  • packages/config/src/features/messages/identifier.ts
  • packages/config/src/shapes.ts
  • packages/plugin-babel/src/index.ts
  • packages/plugin-unplugin/src/index.ts
✅ Files skipped from review due to trivial changes (2)
  • examples/tanstack-start/vite.config.ts
  • packages/plugin-unplugin/src/index.ts

Comment thread packages/config/src/shapes.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/transform-js/src/parser.ts (1)

160-166: Consider renaming getPropertyNameAsString now that it can return a symbol.

The ...AsString suffix is no longer accurate — the function may return AUTO_INCREMENT_IDENTIFIER (a symbol). A name like getPropertyNameAsIdentifier (mirroring getExpressionAsKey/the identifier field it feeds into) would better convey the actual contract and match the terminology used elsewhere in the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/transform-js/src/parser.ts` around lines 160 - 166, Rename the
function getPropertyNameAsString to getPropertyNameAsIdentifier (or similar) to
reflect that it can return a symbol (AUTO_INCREMENT_IDENTIFIER) rather than
strictly a string; update the function declaration and all call sites (notably
where getExpressionAsKey consumes its result into the identifier field) so names
are consistent and tests/type annotations adjusted accordingly to avoid
misleading "...AsString" terminology.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/transform-js/src/parser.ts`:
- Around line 148-156: The code is unsafely casting the result of
getExpressionAsKey to string before constructing an ArgumentMessage; remove the
"as string" cast so key retains its true type (string | typeof
AUTO_INCREMENT_IDENTIFIER) and pass that value directly to new
ArgumentMessage(expression) as used in the existing branch, ensuring you use the
variable from getExpressionAsKey (key) and that ArgumentMessage's constructor
(from packages/config/src/features/messages/types.ts) already accepts the union;
if the constructor signature was changed, adjust it to accept string | typeof
AUTO_INCREMENT_IDENTIFIER rather than forcing a string.
- Line 35: Update unsafe loc.filename accesses and unsound casts: replace direct
accesses like tagged.loc.filename with optional chaining and a safe default
(e.g. tagged.loc?.filename ?? '<unknown>') wherever source location strings are
built (the two places that construct filename:line strings), remove the `as
string` cast when passing key from getExpressionAsKey() into ArgumentMessage so
the symbol AUTO_INCREMENT_IDENTIFIER is preserved, and rename
getPropertyNameAsString to reflect it may return a symbol (e.g.
getPropertyNameAsStringOrSymbol) and update its call sites accordingly.

---

Nitpick comments:
In `@packages/transform-js/src/parser.ts`:
- Around line 160-166: Rename the function getPropertyNameAsString to
getPropertyNameAsIdentifier (or similar) to reflect that it can return a symbol
(AUTO_INCREMENT_IDENTIFIER) rather than strictly a string; update the function
declaration and all call sites (notably where getExpressionAsKey consumes its
result into the identifier field) so names are consistent and tests/type
annotations adjusted accordingly to avoid misleading "...AsString" terminology.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 742a472b-2e9f-4579-a337-af205c58db54

📥 Commits

Reviewing files that changed from the base of the PR and between adea30a and d14a495.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • examples/nextjs/.babelrc
  • examples/nextjs/package.json
  • packages/config/src/commands/extract.ts
  • packages/config/src/shapes.ts
  • packages/plugin-babel/package.json
  • packages/transform-js/src/parser.ts
  • packages/transform-jsx/src/parser.ts
✅ Files skipped from review due to trivial changes (1)
  • examples/nextjs/.babelrc
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/config/src/commands/extract.ts
  • packages/transform-jsx/src/parser.ts
  • packages/config/src/shapes.ts
  • packages/plugin-babel/package.json

Comment thread packages/transform-js/src/parser.ts
Comment thread packages/transform-js/src/parser.ts
@k0d13
Copy link
Copy Markdown
Owner Author

k0d13 commented Apr 18, 2026

@greptile one more review pls

Comment thread packages/config/src/features/catalogue/storage.ts
@k0d13
Copy link
Copy Markdown
Owner Author

k0d13 commented Apr 18, 2026

@greptile will keep reasking for review until its 5/5 ig

Comment thread packages/config/src/features/catalogue/extractor.ts Outdated
@k0d13
Copy link
Copy Markdown
Owner Author

k0d13 commented Apr 18, 2026

@greptile review one more time

Comment thread packages/config/src/shapes.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/config/src/shapes.ts (1)

38-49: ⚠️ Potential issue | 🔴 Critical

Use the reducer accumulator when chaining transformers.

Line 47 still calls t.transform(code, id), so earlier transformer output is discarded whenever more than one transformer matches. Feed p into the next transformer instead.

🐛 Proposed fix
-            transform: (code: string, id: string) =>
-              t.reduce((p, t) => (t.match(id) ? t.transform(code, id) : p), code),
+            transform: (code: string, id: string) =>
+              t.reduce((p, t) => (t.match(id) ? t.transform(p, id) : p), code),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/shapes.ts` around lines 38 - 49, The reducer currently
ignores the accumulated transformed code by calling t.transform(code, id) inside
the reduce in the Transformer.transform chain; update the reducer in the
transform block so the accumulator p is passed into each next transformer (call
t.transform(p, id) instead of t.transform(code, id)) so earlier transformer
output is fed into subsequent transformers and the reduce logic in the
Transformer transform implementation works correctly.
examples/nextjs/package.json (1)

15-15: ⚠️ Potential issue | 🟠 Major

Verify next@16.2.4 is published before merging.

Line 15 still pins the previously flagged version. The registry metadata I could inspect for next did not show 16.2.4 in the listed releases, so this can break installs if npm still lacks that version: https://packages.ecosyste.ms/registries/npmjs.org/packages/next/versions

Run this read-only check against npm and pin to the latest published version if it fails:

#!/bin/bash
# Description: Verify whether the pinned Next.js version exists on npm.

set -euo pipefail

npm view next@16.2.4 version
npm view next dist-tags --json
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/nextjs/package.json` at line 15, The package.json currently pins
"next" to 16.2.4 but that version may not be published; run an npm registry
check (e.g., npm view next@16.2.4 version and npm view next dist-tags) to
confirm existence, and if it’s missing update the "next" entry in package.json
(the dependency key "next") to a published version (either the latest dist-tag
or a specific published semver) or change to a valid tag like "latest" before
merging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@examples/nextjs/package.json`:
- Line 15: The package.json currently pins "next" to 16.2.4 but that version may
not be published; run an npm registry check (e.g., npm view next@16.2.4 version
and npm view next dist-tags) to confirm existence, and if it’s missing update
the "next" entry in package.json (the dependency key "next") to a published
version (either the latest dist-tag or a specific published semver) or change to
a valid tag like "latest" before merging.

In `@packages/config/src/shapes.ts`:
- Around line 38-49: The reducer currently ignores the accumulated transformed
code by calling t.transform(code, id) inside the reduce in the
Transformer.transform chain; update the reducer in the transform block so the
accumulator p is passed into each next transformer (call t.transform(p, id)
instead of t.transform(code, id)) so earlier transformer output is fed into
subsequent transformers and the reduce logic in the Transformer transform
implementation works correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8d1a21ab-3060-4cf2-8bc6-0ca67626ca52

📥 Commits

Reviewing files that changed from the base of the PR and between d14a495 and d227ae2.

📒 Files selected for processing (16)
  • .changeset/config.json
  • .changeset/nine-zebras-greet.md
  • .changeset/silent-moose-wish.md
  • .github/labels.yml
  • examples/nextjs/package.json
  • packages/config/src/features/catalogue/extractor.ts
  • packages/config/src/features/loader/resolve.ts
  • packages/config/src/features/workers/extract-worker.ts
  • packages/config/src/shapes.ts
  • packages/plugin-babel/src/index.ts
  • packages/plugin-unplugin/src/index.ts
  • packages/transform-js/src/index.ts
  • packages/transform-js/src/parser.ts
  • packages/transform-jsx/src/index.ts
  • website/content/core-concepts/configuration.mdx
  • website/content/getting-started/installation.mdx
✅ Files skipped from review due to trivial changes (7)
  • .github/labels.yml
  • .changeset/nine-zebras-greet.md
  • .changeset/silent-moose-wish.md
  • packages/config/src/features/catalogue/extractor.ts
  • website/content/getting-started/installation.mdx
  • packages/plugin-unplugin/src/index.ts
  • packages/plugin-babel/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • .changeset/config.json
  • website/content/core-concepts/configuration.mdx
  • packages/config/src/features/workers/extract-worker.ts
  • packages/transform-jsx/src/index.ts
  • packages/transform-js/src/parser.ts

@k0d13
Copy link
Copy Markdown
Owner Author

k0d13 commented Apr 18, 2026

@greptile one more review pls, say 5/5 this time

@k0d13 k0d13 merged commit d7101cb into main Apr 18, 2026
10 checks passed
@k0d13 k0d13 deleted the transform branch April 18, 2026 08:49
@k0d13 k0d13 restored the transform branch April 19, 2026 18:00
@k0d13 k0d13 deleted the transform branch April 22, 2026 23:44
@k0d13 k0d13 restored the transform branch April 22, 2026 23:44
@k0d13 k0d13 deleted the transform branch April 22, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Updates or changes related to project dependencies examples Updates or additions to example apps package: babel-plugin Related to babel-plugin-saykit package: carbon Related to @saykit/carbon package: config Related to @saykit/config and the CLI package: format-po Related to @saykit/format-po package: react Related to @saykit/react package: unplugin Related to unplugin-saykit tests Modifications, additions, or fixes related to testing website Updates to the documentation website

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom extractors and transformers

1 participant