Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/export-appearance-from-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/ui': patch
'@clerk/upgrade': patch
---

Export `Appearance` type from `@clerk/ui` root entry
2 changes: 2 additions & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Ui } from './internal';
import { UI_BRAND } from './internal';
import type { Appearance } from './internal/appearance';

export type { Appearance } from './internal/appearance';

import { ClerkUI } from './ClerkUI';

declare const PACKAGE_VERSION: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export const fixtures = [
{
name: 'Rewrites basic type import',
source: `
import type { UserResource, ClerkResource } from '@clerk/types';
`,
output: `
import type { UserResource, ClerkResource } from "@clerk/shared/types";
`,
},
{
name: 'Rewrites value import',
source: `
import { OAUTH_PROVIDERS } from '@clerk/types';
`,
output: `
import { OAUTH_PROVIDERS } from "@clerk/shared/types";
`,
},
{
name: 'Redirects Appearance to @clerk/ui',
source: `
import type { Appearance } from '@clerk/types';
`,
output: `
import type { Appearance } from "@clerk/ui";
`,
},
{
name: 'Splits mixed import with Appearance',
source: `
import type { Appearance, UserResource, ClerkResource } from '@clerk/types';
`,
output: `
import type { UserResource, ClerkResource } from "@clerk/shared/types";
import type { Appearance } from "@clerk/ui";
`,
},
{
name: 'Handles require statements',
source: `
const { UserResource } = require('@clerk/types');
`,
output: `
const { UserResource } = require("@clerk/shared/types");
`,
},
{
name: 'Handles require with Appearance only',
source: `
const { Appearance } = require('@clerk/types');
`,
output: `
const { Appearance } = require("@clerk/ui");
`,
},
{
name: 'Splits mixed require with Appearance',
source: `
const { Appearance, UserResource } = require('@clerk/types');
`,
output: `
const {
UserResource
} = require("@clerk/shared/types");

const {
Appearance
} = require("@clerk/ui");
`,
},
{
name: 'Handles namespace import',
source: `
import * as Types from '@clerk/types';
`,
output: `
import * as Types from "@clerk/shared/types";
`,
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { applyTransform } from 'jscodeshift/dist/testUtils';
import { describe, expect, it } from 'vitest';

import transformer from '../transform-clerk-types-to-shared-types.cjs';
import { fixtures } from './__fixtures__/transform-clerk-types-to-shared-types.fixtures';

describe('transform-clerk-types-to-shared-types', () => {
it.each(fixtures)('$name', ({ source, output }) => {
const result = applyTransform(transformer, {}, { source });

expect(result).toEqual(output.trim());
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const SOURCE_PACKAGE = '@clerk/types';
const TARGET_PACKAGE = '@clerk/shared/types';
const UI_PACKAGE = '@clerk/ui';

/**
* Specifiers that should be redirected to `@clerk/ui` instead of `@clerk/shared/types`.
*/
const UI_SPECIFIERS = new Set(['Appearance']);

/**
* Transforms imports of `@clerk/types` to `@clerk/shared/types`, splitting out
* `Appearance` to `@clerk/ui`.
*
* @param {import('jscodeshift').FileInfo} fileInfo
* @param {import('jscodeshift').API} api
* @returns {string|undefined}
*/
module.exports = function transformClerkTypesToSharedTypes({ source }, { jscodeshift: j }) {
const root = j(source);
let dirty = false;

// --- Transform import declarations ---
root.find(j.ImportDeclaration, { source: { value: SOURCE_PACKAGE } }).forEach(path => {
const node = path.node;
const specifiers = node.specifiers || [];
const importKind = node.importKind;

const uiSpecifiers = [];
const sharedSpecifiers = [];

for (const spec of specifiers) {
if (j.ImportSpecifier.check(spec) && UI_SPECIFIERS.has(spec.imported.name)) {
uiSpecifiers.push(spec);
} else {
sharedSpecifiers.push(spec);
}
}

if (uiSpecifiers.length > 0 && sharedSpecifiers.length > 0) {
// Mixed: split into two imports
const sharedImport = j.importDeclaration(sharedSpecifiers, j.stringLiteral(TARGET_PACKAGE));
if (importKind) {
sharedImport.importKind = importKind;
}
sharedImport.comments = node.comments;

const uiImport = j.importDeclaration(uiSpecifiers, j.stringLiteral(UI_PACKAGE));
if (importKind) {
uiImport.importKind = importKind;
}

j(path).replaceWith([sharedImport, uiImport]);
dirty = true;
return;
}

if (uiSpecifiers.length > 0) {
// Only UI specifiers
node.source.value = UI_PACKAGE;
dirty = true;
return;
}

// Only shared specifiers (or namespace/default imports)
node.source.value = TARGET_PACKAGE;
dirty = true;
});

// --- Transform require calls ---
root
.find(j.VariableDeclarator, {
init: {
callee: { name: 'require' },
arguments: [{ value: SOURCE_PACKAGE }],
},
})
.forEach(path => {
const node = path.node;
const id = node.id;

if (id.type === 'ObjectPattern') {
const uiProperties = [];
const sharedProperties = [];

for (const prop of id.properties) {
if (prop.key && UI_SPECIFIERS.has(prop.key.name)) {
uiProperties.push(prop);
} else {
sharedProperties.push(prop);
}
}

if (uiProperties.length > 0 && sharedProperties.length > 0) {
// Mixed: keep shared on main, create new require for UI
node.id.properties = sharedProperties;
node.init.arguments[0] = j.literal(TARGET_PACKAGE);

const variableDeclaration = path.parent.node;
const kind = variableDeclaration.kind || 'const';

const uiDeclarator = j.variableDeclarator(
j.objectPattern(uiProperties),
j.callExpression(j.identifier('require'), [j.literal(UI_PACKAGE)]),
);
const uiDeclaration = j.variableDeclaration(kind, [uiDeclarator]);

j(path.parent).insertAfter(uiDeclaration);
dirty = true;
return;
}

if (uiProperties.length > 0) {
node.init.arguments[0] = j.literal(UI_PACKAGE);
dirty = true;
return;
}
}

// Only shared or not destructured
node.init.arguments[0] = j.literal(TARGET_PACKAGE);
dirty = true;
});

if (!dirty) {
return undefined;
}

let result = root.toSource();
result = result.replace(/^(['"`][^'"`]+['"`]);;/gm, '$1;');
return result;
};

module.exports.parser = 'tsx';
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,10 @@ Update your imports:
```

The `@clerk/types` package will continue to re-export types from `@clerk/shared/types` for backward compatibility, but new types will only be added to `@clerk/shared/types`.

**Note:** The `Appearance` type should be imported from `@clerk/ui` instead of `@clerk/shared/types`:

```diff
- import type { Appearance } from '@clerk/types';
+ import type { Appearance } from '@clerk/ui';
```
1 change: 1 addition & 0 deletions packages/upgrade/src/versions/core-3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default {
'transform-remove-deprecated-appearance-props',
'transform-appearance-layout-to-options',
'transform-themes-to-ui-themes',
'transform-clerk-types-to-shared-types',
'transform-align-experimental-unstable-prefixes',
// React/JSX version of Protect→Show (handles .tsx, .jsx, .ts, .js files)
{
Expand Down
Loading