diff --git a/packages/@stylexjs/eslint-plugin/__tests__/stylex-enforce-extension-test.js b/packages/@stylexjs/eslint-plugin/__tests__/stylex-enforce-extension-test.js index fc898adb6..4a151dd52 100644 --- a/packages/@stylexjs/eslint-plugin/__tests__/stylex-enforce-extension-test.js +++ b/packages/@stylexjs/eslint-plugin/__tests__/stylex-enforce-extension-test.js @@ -26,6 +26,8 @@ const invalidConstsFilenameWithoutRestrictedExports = (suggestedExtension) => `Only variables from \`stylex.defineConsts()\` can be exported from a file with a \`${suggestedExtension}\` extension.`; const invalidExportFromConstsFiles = 'Files that export variables from `stylex.defineConsts()` must not export anything else.'; +const aliasedExport = (exportedName, localName) => + `\`${exportedName}\` is another name for \`${localName}\`. StyleX derives variable names from the export name, so importing \`${exportedName}\` references variables that are never defined. Export each \`stylex.defineVars()\`, \`stylex.defineConsts()\`, or \`stylex.defineMarker()\` result under exactly one name.`; const invalidDefaultExport = (type) => `Default exports are not allowed for variables from \`stylex.${type}()\`. Use named exports instead.`; @@ -1022,6 +1024,106 @@ ruleTester.run('stylex-enforce-extension', rule.default, { options: [{ enforceDefineConstsExtension: true }], errors: [{ message: invalidExportFromConstsFiles }], }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const vars = stylex.defineVars({ color: 'red' }); + const helper = someFunction(); + export { helper }; + `, + filename: 'myComponent.stylex.jsx', + errors: [{ message: invalidExportFromThemeFiles }], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const vars = stylex.defineVars({ color: 'red' }); + export { helper } from './helper'; + `, + filename: 'myComponent.stylex.jsx', + errors: [{ message: invalidExportFromThemeFiles }], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const consts = stylex.defineConsts({ color: 'red' }); + const helper = someFunction(); + export { helper }; + `, + filename: 'myComponent.stylex.const.jsx', + options: [{ enforceDefineConstsExtension: true }], + errors: [{ message: invalidExportFromConstsFiles }], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const vars = stylex.defineVars({ color: 'red' }); + export * from './helpers'; + `, + filename: 'myComponent.stylex.jsx', + errors: [{ message: invalidExportFromThemeFiles }], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export * as helpers from './helpers'; + export const vars = stylex.defineVars({ color: 'red' }); + `, + filename: 'myComponent.stylex.jsx', + errors: [ + { message: invalidFilenameWithoutRestrictedExports('.stylex.jsx') }, + { message: invalidExportFromThemeFiles }, + ], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const consts = stylex.defineConsts({ color: 'red' }); + export * from './helpers'; + `, + filename: 'myComponent.stylex.const.jsx', + options: [{ enforceDefineConstsExtension: true }], + errors: [{ message: invalidExportFromConstsFiles }], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const themeNested = stylex.unstable_defineConstsNested({ + color: { primary: 'red' }, + }); + export { themeNested as theme }; + `, + filename: 'theme.stylex.jsx', + errors: [{ message: aliasedExport('theme', 'themeNested') }], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const vars = stylex.defineVars({ color: 'red' }); + export const colors = vars; + `, + filename: 'theme.stylex.jsx', + errors: [{ message: aliasedExport('colors', 'vars') }], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const marker = stylex.defineMarker(); + export { marker as cardMarker }; + `, + filename: 'markers.stylex.jsx', + errors: [{ message: aliasedExport('cardMarker', 'marker') }], + }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const vars = stylex.defineVars({ color: 'red' }); + export const colors = vars; + `, + filename: 'theme.stylex.jsx', + options: [{ legacyAllowMixedExports: true }], + errors: [{ message: aliasedExport('colors', 'vars') }], + }, ], }); @@ -1083,6 +1185,17 @@ typeExportRuleTester.run( `, filename: 'colors.stylex.ts', }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + export const colors = stylex.defineVars({ + primary: 'blue', + secondary: 'green', + }); + export type * from './types'; + `, + filename: 'colors.stylex.ts', + }, ], invalid: [ @@ -1096,6 +1209,17 @@ typeExportRuleTester.run( filename: 'colors.stylex.ts', errors: [{ message: invalidExportFromThemeFiles }], }, + { + code: ` + import * as stylex from '@stylexjs/stylex'; + type Colors = keyof typeof colors; + const helper = someFunction(); + export const colors = stylex.defineVars({ primary: 'blue' }); + export { type Colors, helper }; + `, + filename: 'colors.stylex.ts', + errors: [{ message: invalidExportFromThemeFiles }], + }, ], }, ); diff --git a/packages/@stylexjs/eslint-plugin/src/stylex-enforce-extension.js b/packages/@stylexjs/eslint-plugin/src/stylex-enforce-extension.js index ac03485d7..5fc7b21e2 100644 --- a/packages/@stylexjs/eslint-plugin/src/stylex-enforce-extension.js +++ b/packages/@stylexjs/eslint-plugin/src/stylex-enforce-extension.js @@ -264,11 +264,60 @@ const stylexEnforceExtension = { return false; } + function isDefineVariable(name: ?string): boolean { + return ( + name != null && + (defineVarsVariables.has(name) || + defineConstsVariables.has(name) || + defineMarkerVariables.has(name)) + ); + } + + function reportAliasedExport( + node: Node, + localName: string, + exportedName: string, + ): void { + context.report({ + node, + message: `\`${exportedName}\` is another name for \`${localName}\`. StyleX derives variable names from the export name, so importing \`${exportedName}\` references variables that are never defined. Export each \`stylex.defineVars()\`, \`stylex.defineConsts()\`, or \`stylex.defineMarker()\` result under exactly one name.`, + }); + } + + function checkExportSpecifiers(node: Node): void { + const specifiers = (node as any).specifiers || []; + specifiers.forEach((specifier: any) => { + if (specifier.exportKind === 'type') { + return; + } + const localName = specifier.local?.name; + const exportedName = + specifier.exported?.name ?? specifier.exported?.value; + if ( + (node as any).source == null && + localName !== exportedName && + isDefineVariable(localName) + ) { + reportAliasedExport(specifier, localName, exportedName); + } else { + hasOtherExports = true; + } + }); + } + function checkExports(node: Node): void { const declaration = (node as any).declaration; const isDefaultExport = node.type === 'ExportDefaultDeclaration'; - if (!declaration) return; + if (node.type === 'ExportAllDeclaration') { + hasOtherExports = true; + return; + } + + if (!declaration) { + checkExportSpecifiers(node); + return; + } if (isDefaultExport && checkDefaultExport(node, declaration)) { return; @@ -303,6 +352,18 @@ const stylexEnforceExtension = { ) { defineMarkerVariables.add(decl.id.name); } + } else if ( + decl.type === 'VariableDeclarator' && + decl.id?.type === 'Identifier' && + decl.init?.type === 'Identifier' + ) { + const exportedName = decl.id.name; + const localName = decl.init.name; + if (isDefineVariable(localName)) { + reportAliasedExport(decl, localName, exportedName); + } else { + hasOtherExports = true; + } } else { hasOtherExports = true; } @@ -430,7 +491,9 @@ const stylexEnforceExtension = { } }); }, - 'ExportNamedDeclaration, ExportDefaultDeclaration'(node: Node): void { + 'ExportNamedDeclaration, ExportDefaultDeclaration, ExportAllDeclaration'( + node: Node, + ): void { if ((node as any).exportKind === 'type') { return; }