From a0ecf5d873891d89db9aeb5a3c7326ba4ff17e35 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 22 Jan 2026 22:16:56 +0200 Subject: [PATCH 1/2] feat(upgrade): add satellite auto-sync codemod for core 3 migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Core 3 changed satellite behavior — apps no longer auto-sync on first visit unless `satelliteAutoSync: true` is set. This codemod preserves Core 2 behavior by adding the prop wherever `isSatellite` is configured. --- .../transform-satellite-auto-sync.fixtures.js | 173 ++++++++++++++++++ .../transform-satellite-auto-sync.test.js | 17 ++ .../transform-satellite-auto-sync.cjs | 74 ++++++++ packages/upgrade/src/versions/core-3/index.js | 1 + 4 files changed, 265 insertions(+) create mode 100644 packages/upgrade/src/codemods/__tests__/__fixtures__/transform-satellite-auto-sync.fixtures.js create mode 100644 packages/upgrade/src/codemods/__tests__/transform-satellite-auto-sync.test.js create mode 100644 packages/upgrade/src/codemods/transform-satellite-auto-sync.cjs diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-satellite-auto-sync.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-satellite-auto-sync.fixtures.js new file mode 100644 index 00000000000..88343606e85 --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-satellite-auto-sync.fixtures.js @@ -0,0 +1,173 @@ +export const fixtures = [ + { + name: 'JSX: isSatellite={true}', + source: ` +import { ClerkProvider } from '@clerk/nextjs'; + +export function App({ children }) { + return ( + + {children} + + ); +} + `, + output: ` +import { ClerkProvider } from '@clerk/nextjs'; + +export function App({ children }) { + return ( + + {children} + + ); +} + `, + }, + { + name: 'JSX: isSatellite (boolean shorthand)', + source: ` +import { ClerkProvider } from '@clerk/react'; + +export const App = () => ( + +
+ +); + `, + output: ` +import { ClerkProvider } from '@clerk/react'; + +export const App = () => ( + +
+ +); + `, + }, + { + name: 'JSX: isSatellite with function value', + source: ` +import { ClerkProvider } from '@clerk/nextjs'; + +export function App({ children }) { + return ( + url.host === 'satellite.example.com'} domain="example.com"> + {children} + + ); +} + `, + output: ` +import { ClerkProvider } from '@clerk/nextjs'; + +export function App({ children }) { + return ( + url.host === 'satellite.example.com'} + domain="example.com" + satelliteAutoSync={true}> + {children} + + ); +} + `, + }, + { + name: 'JSX: already has satelliteAutoSync', + source: ` +import { ClerkProvider } from '@clerk/nextjs'; + +export function App({ children }) { + return ( + + {children} + + ); +} + `, + noChange: true, + }, + { + name: 'Object: isSatellite in clerkMiddleware options', + source: ` +import { clerkMiddleware } from '@clerk/nextjs/server'; + +export default clerkMiddleware({ + isSatellite: true, + domain: 'example.com', +}); + `, + output: ` +import { clerkMiddleware } from '@clerk/nextjs/server'; + +export default clerkMiddleware({ + isSatellite: true, + satelliteAutoSync: true, + domain: 'example.com' +}); + `, + }, + { + name: 'Object: isSatellite in variable declaration', + source: ` +const options = { + isSatellite: true, + domain: 'satellite.example.com', +}; + `, + output: ` +const options = { + isSatellite: true, + satelliteAutoSync: true, + domain: 'satellite.example.com' +}; + `, + }, + { + name: 'Object: isSatellite with function value', + source: ` +import { clerkMiddleware } from '@clerk/nextjs/server'; + +export default clerkMiddleware({ + isSatellite: (url) => url.host === 'satellite.example.com', + domain: 'example.com', +}); + `, + output: ` +import { clerkMiddleware } from '@clerk/nextjs/server'; + +export default clerkMiddleware({ + isSatellite: (url) => url.host === 'satellite.example.com', + satelliteAutoSync: true, + domain: 'example.com' +}); + `, + }, + { + name: 'Object: already has satelliteAutoSync', + source: ` +const options = { + isSatellite: true, + satelliteAutoSync: false, + domain: 'example.com', +}; + `, + noChange: true, + }, + { + name: 'No isSatellite present (no changes)', + source: ` +import { ClerkProvider } from '@clerk/nextjs'; + +export function App({ children }) { + return ( + + {children} + + ); +} + `, + noChange: true, + }, +]; diff --git a/packages/upgrade/src/codemods/__tests__/transform-satellite-auto-sync.test.js b/packages/upgrade/src/codemods/__tests__/transform-satellite-auto-sync.test.js new file mode 100644 index 00000000000..a9be8e56e98 --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/transform-satellite-auto-sync.test.js @@ -0,0 +1,17 @@ +import { applyTransform } from 'jscodeshift/dist/testUtils'; +import { describe, expect, it } from 'vitest'; + +import transformer from '../transform-satellite-auto-sync.cjs'; +import { fixtures } from './__fixtures__/transform-satellite-auto-sync.fixtures'; + +describe('transform-satellite-auto-sync', () => { + it.each(fixtures)('$name', ({ source, output, noChange }) => { + const result = applyTransform(transformer, {}, { source }); + + if (noChange) { + expect(result).toEqual(''); + } else { + expect(result).toEqual(output.trim()); + } + }); +}); diff --git a/packages/upgrade/src/codemods/transform-satellite-auto-sync.cjs b/packages/upgrade/src/codemods/transform-satellite-auto-sync.cjs new file mode 100644 index 00000000000..4deb5506cd4 --- /dev/null +++ b/packages/upgrade/src/codemods/transform-satellite-auto-sync.cjs @@ -0,0 +1,74 @@ +module.exports = function transformSatelliteAutoSync({ source }, { jscodeshift: j }) { + const root = j(source); + let dirty = false; + + // Handle JSX attributes: + root.find(j.JSXOpeningElement).forEach(path => { + const { attributes } = path.node; + if (!attributes) { + return; + } + + const hasIsSatellite = attributes.some(attr => isJsxAttrNamed(attr, 'isSatellite')); + if (!hasIsSatellite) { + return; + } + + const hasAutoSync = attributes.some(attr => isJsxAttrNamed(attr, 'satelliteAutoSync')); + if (hasAutoSync) { + return; + } + + const autoSyncAttr = j.jsxAttribute( + j.jsxIdentifier('satelliteAutoSync'), + j.jsxExpressionContainer(j.booleanLiteral(true)), + ); + attributes.push(autoSyncAttr); + dirty = true; + }); + + // Handle object properties: { isSatellite: true } → { isSatellite: true, satelliteAutoSync: true } + root.find(j.ObjectExpression).forEach(path => { + const { properties } = path.node; + + const hasIsSatellite = properties.some(prop => isObjectPropertyNamed(prop, 'isSatellite')); + if (!hasIsSatellite) { + return; + } + + const hasAutoSync = properties.some(prop => isObjectPropertyNamed(prop, 'satelliteAutoSync')); + if (hasAutoSync) { + return; + } + + const isSatelliteIndex = properties.findIndex(prop => isObjectPropertyNamed(prop, 'isSatellite')); + const autoSyncProp = j.objectProperty(j.identifier('satelliteAutoSync'), j.booleanLiteral(true)); + properties.splice(isSatelliteIndex + 1, 0, autoSyncProp); + dirty = true; + }); + + return dirty ? root.toSource() : undefined; +}; + +module.exports.parser = 'tsx'; + +function isJsxAttrNamed(attribute, name) { + return attribute && attribute.type === 'JSXAttribute' && attribute.name && attribute.name.name === name; +} + +function isObjectPropertyNamed(prop, name) { + if (!prop || (prop.type !== 'ObjectProperty' && prop.type !== 'Property')) { + return false; + } + const { key } = prop; + if (!key) { + return false; + } + if (key.type === 'Identifier') { + return key.name === name; + } + if (key.type === 'StringLiteral' || key.type === 'Literal') { + return key.value === name; + } + return false; +} diff --git a/packages/upgrade/src/versions/core-3/index.js b/packages/upgrade/src/versions/core-3/index.js index 2a22f7640c5..ab4ba699da5 100644 --- a/packages/upgrade/src/versions/core-3/index.js +++ b/packages/upgrade/src/versions/core-3/index.js @@ -29,5 +29,6 @@ export default { { name: 'transform-clerk-provider-inside-body', packages: ['nextjs'] }, // Migrate @clerk/react-router/api.server → @clerk/react-router/server { name: 'transform-react-router-api-server', packages: ['react-router'] }, + { name: 'transform-satellite-auto-sync', packages: ['nextjs', 'react', 'expo', 'astro', 'tanstack-react-start'] }, ], }; From b4d0d965fc281c7492d2164a7084198c8f7a1fcd Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 22 Jan 2026 22:20:36 +0200 Subject: [PATCH 2/2] chore(upgrade): add changeset --- .changeset/satellite-auto-sync-codemod.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/satellite-auto-sync-codemod.md diff --git a/.changeset/satellite-auto-sync-codemod.md b/.changeset/satellite-auto-sync-codemod.md new file mode 100644 index 00000000000..c87902fe554 --- /dev/null +++ b/.changeset/satellite-auto-sync-codemod.md @@ -0,0 +1,5 @@ +--- +'@clerk/upgrade': minor +--- + +Add `transform-satellite-auto-sync` codemod for Core 3 migration that adds `satelliteAutoSync: true` wherever `isSatellite` is configured