@tenphi/eslint-plugin-tasty 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -168,9 +168,21 @@ function isModuleTastyComponent(fromFile, specifier, exportName, options, visite
168
168
  return info.starSources.some((source) => isModuleTastyComponent(filename, source, exportName, options, visited));
169
169
  }
170
170
  /**
171
+ * An origin with more members read off it. A factory wrapper's members are its
172
+ * own sub-elements, not the wrapped component's, so they lead nowhere.
173
+ */
174
+ function withMembers(origin, members) {
175
+ if (!origin || members.length === 0) return origin;
176
+ if (origin.wrapped) return null;
177
+ return {
178
+ ...origin,
179
+ members: [...origin.members, ...members]
180
+ };
181
+ }
182
+ /**
171
183
  * Follow `exportName` of the module `specifier` (relative to `fromFile`) to the
172
184
  * package import it comes from: through re-exports, `const` aliases, namespace
173
- * members (`UI.Button` is `Button`, `Text.Strong` is `Text`), and factory
185
+ * members (`UI.Button` is `Button`), member reads (`Button.Group`), and factory
174
186
  * wrappers whose first argument is the component they extend.
175
187
  */
176
188
  function resolveModuleExportOrigin(fromFile, specifier, exportName, options, visited = /* @__PURE__ */ new Set()) {
@@ -183,39 +195,43 @@ function resolveModuleExportOrigin(fromFile, specifier, exportName, options, vis
183
195
  const info = readModule(filename);
184
196
  if (!info) return null;
185
197
  const T = getTypeScript();
186
- const fromImport = (binding, member) => {
187
- const name = binding.imported === "*" ? member : binding.imported;
198
+ /** `members` are read off the binding, so a namespace's first one is the export. */
199
+ const fromImport = (binding, members = []) => {
200
+ const namespace = binding.imported === "*";
201
+ const name = namespace ? members[0] : binding.imported;
202
+ const rest = namespace ? members.slice(1) : members;
188
203
  if (name === void 0) return null;
189
204
  if (!binding.source.startsWith(".")) return {
190
205
  source: binding.source,
191
206
  name,
207
+ members: rest,
192
208
  wrapped: false
193
209
  };
194
- return resolveModuleExportOrigin(filename, binding.source, name, options, visited);
210
+ return withMembers(resolveModuleExportOrigin(filename, binding.source, name, options, visited), rest);
195
211
  };
196
212
  const seenLocals = /* @__PURE__ */ new Set();
197
- const fromLocal = (name, member) => {
198
- const seen = `${name}\0${member ?? ""}`;
213
+ const fromLocal = (name, members = []) => {
214
+ const seen = [name, ...members].join(".");
199
215
  if (seenLocals.has(seen)) return null;
200
216
  seenLocals.add(seen);
201
217
  const init = info.constants.get(name);
202
- if (init) return fromExpression(init, member);
218
+ if (init) return fromExpression(init, members);
203
219
  const binding = info.imports.get(name);
204
- return binding ? fromImport(binding, member) : null;
220
+ return binding ? fromImport(binding, members) : null;
205
221
  };
206
- const fromExpression = (node, member) => {
222
+ const fromExpression = (node, members = []) => {
207
223
  while (T.isParenthesizedExpression(node) || T.isAsExpression(node) || T.isSatisfiesExpression(node) || T.isNonNullExpression(node) || T.isTypeAssertionExpression(node)) node = node.expression;
208
- if (T.isIdentifier(node)) return fromLocal(node.text, member);
224
+ if (T.isIdentifier(node)) return fromLocal(node.text, members);
209
225
  if (T.isPropertyAccessExpression(node)) {
210
226
  let root = node;
211
- let first = member;
227
+ const path = [...members];
212
228
  while (T.isPropertyAccessExpression(root)) {
213
- first = root.name.text;
229
+ path.unshift(root.name.text);
214
230
  root = root.expression;
215
231
  }
216
- return T.isIdentifier(root) ? fromLocal(root.text, first) : null;
232
+ return T.isIdentifier(root) ? fromLocal(root.text, path) : null;
217
233
  }
218
- if (member === void 0 && T.isCallExpression(node) && T.isIdentifier(node.expression)) {
234
+ if (members.length === 0 && T.isCallExpression(node) && T.isIdentifier(node.expression)) {
219
235
  const binding = info.imports.get(node.expression.text);
220
236
  const base = node.arguments[0];
221
237
  if (!binding || !options.importSources.has(binding.source) || !options.isComponentFactory(binding.imported) || !base || T.isObjectLiteralExpression(base)) return null;
@@ -237,6 +253,7 @@ function resolveModuleExportOrigin(fromFile, specifier, exportName, options, vis
237
253
  const origin = source.startsWith(".") ? resolveModuleExportOrigin(filename, source, exportName, options, visited) : options.isOriginSource(source) ? {
238
254
  source,
239
255
  name: exportName,
256
+ members: [],
240
257
  wrapped: false
241
258
  } : null;
242
259
  if (origin) return origin;
@@ -245,5 +262,5 @@ function resolveModuleExportOrigin(fromFile, specifier, exportName, options, vis
245
262
  }
246
263
 
247
264
  //#endregion
248
- export { isModuleTastyComponent, resolveModuleExportOrigin };
265
+ export { isModuleTastyComponent, resolveModuleExportOrigin, withMembers };
249
266
  //# sourceMappingURL=module-components.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"module-components.js","names":[],"sources":["../src/module-components.ts"],"sourcesContent":["import { readFileSync, statSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, extname, join, resolve } from 'node:path';\nimport type ts from 'typescript';\n\n/**\n * Whether an export of a relative module is a Tasty component.\n *\n * Elements of a design-system component usually live next to it in `styled.ts`\n * or `elements.ts`, so a rule that only recognises `tasty()` results declared in\n * the file being linted misses most of them. This reads the imported module the\n * same syntactic way: an export is a component when it is a `tasty()` call (or a\n * configured options-kind factory), a `const` alias of one, an import from a\n * configured source, or a re-export that leads to one of those.\n *\n * Parsing goes through TypeScript, an optional peer dependency. Without it, an\n * imported component is simply not recognised, which is how it was before.\n */\nexport interface ModuleComponentOptions {\n importSources: ReadonlySet<string>;\n /** Imported names whose call result is a Tasty component. */\n isComponentFactory(importedName: string): boolean;\n}\n\ntype TypeScript = typeof ts;\n\ninterface ImportBinding {\n source: string;\n /** `'default'`, `'*'` for a namespace, or the exported name. */\n imported: string;\n}\n\ninterface ModuleInfo {\n imports: Map<string, ImportBinding>;\n constants: Map<string, ts.Expression>;\n /** Exported name -> local name. */\n exports: Map<string, string>;\n reexports: Map<string, ImportBinding>;\n starSources: string[];\n defaultExpression?: ts.Expression;\n}\n\nconst require = createRequire(import.meta.url);\nlet typescript: TypeScript | null | undefined;\nconst modules = new Map<string, { version: string; info: ModuleInfo }>();\n\nconst EXTENSIONS = [\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n];\n// A TypeScript ESM import names the emitted file: `./styled.js` is `styled.ts`.\nconst SOURCE_EXTENSIONS: Record<string, string[]> = {\n '.js': ['.ts', '.tsx'],\n '.jsx': ['.tsx'],\n '.mjs': ['.mts'],\n '.cjs': ['.cts'],\n};\n\nfunction getTypeScript(): TypeScript | null {\n if (typescript === undefined) {\n try {\n const module = require('typescript') as Partial<TypeScript>;\n // TypeScript 7 (the native port) exports only its version.\n typescript =\n typeof module.createSourceFile === 'function'\n ? (module as TypeScript)\n : null;\n } catch {\n typescript = null;\n }\n }\n return typescript;\n}\n\nfunction version(filename: string): string | null {\n try {\n const stat = statSync(filename);\n return stat.isFile() ? `${stat.mtimeMs}:${stat.size}` : null;\n } catch {\n return null;\n }\n}\n\n/** Resolve a relative specifier the way a bundler would, without `paths`. */\nexport function resolveRelativeModule(\n fromFile: string,\n specifier: string,\n): string | null {\n const base = resolve(dirname(fromFile), specifier);\n const extension = extname(base);\n const candidates = [\n ...(EXTENSIONS.includes(extension) ? [base] : []),\n ...(SOURCE_EXTENSIONS[extension] ?? []).map(\n (replacement) => base.slice(0, -extension.length) + replacement,\n ),\n ...EXTENSIONS.map((ext) => base + ext),\n ...EXTENSIONS.map((ext) => join(base, 'index' + ext)),\n ];\n\n return candidates.find((candidate) => version(candidate) !== null) ?? null;\n}\n\nfunction hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {\n const T = getTypeScript()!;\n return (\n T.canHaveModifiers(node) &&\n (T.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false)\n );\n}\n\nfunction readModule(filename: string): ModuleInfo | null {\n const T = getTypeScript();\n const current = version(filename);\n if (!T || current === null) return null;\n\n const cached = modules.get(filename);\n if (cached?.version === current) return cached.info;\n\n const source = T.createSourceFile(\n filename,\n readFileSync(filename, 'utf8'),\n T.ScriptTarget.Latest,\n );\n const info: ModuleInfo = {\n imports: new Map(),\n constants: new Map(),\n exports: new Map(),\n reexports: new Map(),\n starSources: [],\n };\n\n for (const statement of source.statements) {\n if (T.isImportDeclaration(statement)) {\n const clause = statement.importClause;\n if (!clause || clause.isTypeOnly) continue;\n if (!T.isStringLiteral(statement.moduleSpecifier)) continue;\n const from = statement.moduleSpecifier.text;\n if (clause.name) {\n info.imports.set(clause.name.text, {\n source: from,\n imported: 'default',\n });\n }\n const bindings = clause.namedBindings;\n if (bindings && T.isNamespaceImport(bindings)) {\n info.imports.set(bindings.name.text, { source: from, imported: '*' });\n } else if (bindings) {\n for (const element of bindings.elements) {\n if (element.isTypeOnly) continue;\n info.imports.set(element.name.text, {\n source: from,\n imported: (element.propertyName ?? element.name).text,\n });\n }\n }\n } else if (T.isVariableStatement(statement)) {\n if (!(statement.declarationList.flags & T.NodeFlags.Const)) continue;\n const exported = hasModifier(statement, T.SyntaxKind.ExportKeyword);\n for (const declaration of statement.declarationList.declarations) {\n if (!T.isIdentifier(declaration.name) || !declaration.initializer) {\n continue;\n }\n const name = declaration.name.text;\n info.constants.set(name, declaration.initializer);\n if (exported) info.exports.set(name, name);\n }\n } else if (T.isExportDeclaration(statement)) {\n if (statement.isTypeOnly) continue;\n const from =\n statement.moduleSpecifier &&\n T.isStringLiteral(statement.moduleSpecifier)\n ? statement.moduleSpecifier.text\n : null;\n const clause = statement.exportClause;\n if (!clause) {\n if (from !== null) info.starSources.push(from);\n } else if (T.isNamedExports(clause)) {\n for (const element of clause.elements) {\n if (element.isTypeOnly) continue;\n const local = (element.propertyName ?? element.name).text;\n if (from === null) info.exports.set(element.name.text, local);\n else\n info.reexports.set(element.name.text, {\n source: from,\n imported: local,\n });\n }\n }\n } else if (T.isExportAssignment(statement) && !statement.isExportEquals) {\n info.defaultExpression = statement.expression;\n }\n }\n\n modules.set(filename, { version: current, info });\n return info;\n}\n\n/** Whether `exportName` of the module `specifier` (relative to `fromFile`) is a Tasty component. */\nexport function isModuleTastyComponent(\n fromFile: string,\n specifier: string,\n exportName: string,\n options: ModuleComponentOptions,\n visited = new Set<string>(),\n): boolean {\n if (!specifier.startsWith('.')) return false;\n const filename = resolveRelativeModule(fromFile, specifier);\n if (filename === null) return false;\n\n const key = `${filename}\\0${exportName}`;\n if (visited.has(key)) return false;\n visited.add(key);\n\n const info = readModule(filename);\n if (!info) return false;\n const T = getTypeScript()!;\n\n const fromImport = (binding: ImportBinding): boolean =>\n options.importSources.has(binding.source) ||\n (binding.imported !== '*' &&\n isModuleTastyComponent(\n filename,\n binding.source,\n binding.imported,\n options,\n visited,\n ));\n\n const seenLocals = new Set<string>();\n const fromLocal = (name: string): boolean => {\n if (seenLocals.has(name)) return false;\n seenLocals.add(name);\n const init = info.constants.get(name);\n if (init) return fromExpression(init);\n const binding = info.imports.get(name);\n return binding !== undefined && fromImport(binding);\n };\n\n const fromExpression = (node: ts.Expression): boolean => {\n while (\n T.isParenthesizedExpression(node) ||\n T.isAsExpression(node) ||\n T.isSatisfiesExpression(node) ||\n T.isNonNullExpression(node) ||\n T.isTypeAssertionExpression(node)\n ) {\n node = node.expression;\n }\n if (T.isIdentifier(node)) return fromLocal(node.text);\n if (T.isPropertyAccessExpression(node)) {\n // `UI.Panel` from `import * as UI from './ui'` is that module's `Panel`.\n if (T.isIdentifier(node.expression)) {\n const binding = info.imports.get(node.expression.text);\n if (\n binding?.imported === '*' &&\n !options.importSources.has(binding.source)\n ) {\n return isModuleTastyComponent(\n filename,\n binding.source,\n node.name.text,\n options,\n visited,\n );\n }\n }\n let root: ts.Expression = node;\n while (T.isPropertyAccessExpression(root)) root = root.expression;\n return T.isIdentifier(root) && fromLocal(root.text);\n }\n if (T.isCallExpression(node) && T.isIdentifier(node.expression)) {\n const binding = info.imports.get(node.expression.text);\n return (\n binding !== undefined &&\n options.importSources.has(binding.source) &&\n options.isComponentFactory(binding.imported)\n );\n }\n return false;\n };\n\n const local = info.exports.get(exportName);\n if (local !== undefined) return fromLocal(local);\n if (exportName === 'default' && info.defaultExpression) {\n return fromExpression(info.defaultExpression);\n }\n const reexport = info.reexports.get(exportName);\n if (reexport) return fromImport(reexport);\n if (exportName === 'default') return false;\n\n return info.starSources.some((source) =>\n isModuleTastyComponent(filename, source, exportName, options, visited),\n );\n}\n\n/** The package import an export of a relative module comes from. */\nexport interface ModuleExportOrigin {\n /** The package specifier, as written in the import. */\n source: string;\n /** The name the package exports it under. */\n name: string;\n /** Whether a component factory call, such as `tasty(Button)`, wraps it. */\n wrapped: boolean;\n}\n\nexport interface ModuleOriginOptions extends ModuleComponentOptions {\n /** Whether `export * from source` may provide the name being followed. */\n isOriginSource(source: string): boolean;\n}\n\n/**\n * Follow `exportName` of the module `specifier` (relative to `fromFile`) to the\n * package import it comes from: through re-exports, `const` aliases, namespace\n * members (`UI.Button` is `Button`, `Text.Strong` is `Text`), and factory\n * wrappers whose first argument is the component they extend.\n */\nexport function resolveModuleExportOrigin(\n fromFile: string,\n specifier: string,\n exportName: string,\n options: ModuleOriginOptions,\n visited = new Set<string>(),\n): ModuleExportOrigin | null {\n if (!specifier.startsWith('.')) return null;\n const filename = resolveRelativeModule(fromFile, specifier);\n if (filename === null) return null;\n\n const key = `${filename}\\0${exportName}`;\n if (visited.has(key)) return null;\n visited.add(key);\n\n const info = readModule(filename);\n if (!info) return null;\n const T = getTypeScript()!;\n\n const fromImport = (\n binding: ImportBinding,\n member?: string,\n ): ModuleExportOrigin | null => {\n const name = binding.imported === '*' ? member : binding.imported;\n if (name === undefined) return null;\n if (!binding.source.startsWith('.')) {\n return { source: binding.source, name, wrapped: false };\n }\n return resolveModuleExportOrigin(\n filename,\n binding.source,\n name,\n options,\n visited,\n );\n };\n\n const seenLocals = new Set<string>();\n const fromLocal = (\n name: string,\n member?: string,\n ): ModuleExportOrigin | null => {\n const seen = `${name}\\0${member ?? ''}`;\n if (seenLocals.has(seen)) return null;\n seenLocals.add(seen);\n const init = info.constants.get(name);\n if (init) return fromExpression(init, member);\n const binding = info.imports.get(name);\n return binding ? fromImport(binding, member) : null;\n };\n\n const fromExpression = (\n node: ts.Expression,\n member?: string,\n ): ModuleExportOrigin | null => {\n while (\n T.isParenthesizedExpression(node) ||\n T.isAsExpression(node) ||\n T.isSatisfiesExpression(node) ||\n T.isNonNullExpression(node) ||\n T.isTypeAssertionExpression(node)\n ) {\n node = node.expression;\n }\n if (T.isIdentifier(node)) return fromLocal(node.text, member);\n if (T.isPropertyAccessExpression(node)) {\n let root: ts.Expression = node;\n let first = member;\n while (T.isPropertyAccessExpression(root)) {\n first = root.name.text;\n root = root.expression;\n }\n return T.isIdentifier(root) ? fromLocal(root.text, first) : null;\n }\n if (\n member === undefined &&\n T.isCallExpression(node) &&\n T.isIdentifier(node.expression)\n ) {\n const binding = info.imports.get(node.expression.text);\n const base = node.arguments[0];\n if (\n !binding ||\n !options.importSources.has(binding.source) ||\n !options.isComponentFactory(binding.imported) ||\n !base ||\n T.isObjectLiteralExpression(base)\n ) {\n return null;\n }\n const origin = fromExpression(base);\n return origin && { ...origin, wrapped: true };\n }\n return null;\n };\n\n const local = info.exports.get(exportName);\n if (local !== undefined) return fromLocal(local);\n if (exportName === 'default' && info.defaultExpression) {\n return fromExpression(info.defaultExpression);\n }\n const reexport = info.reexports.get(exportName);\n if (reexport) return fromImport(reexport);\n if (exportName === 'default') return null;\n\n for (const source of info.starSources) {\n const origin = source.startsWith('.')\n ? resolveModuleExportOrigin(\n filename,\n source,\n exportName,\n options,\n visited,\n )\n : options.isOriginSource(source)\n ? { source, name: exportName, wrapped: false }\n : null;\n if (origin) return origin;\n }\n return null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"module-components.js","names":[],"sources":["../src/module-components.ts"],"sourcesContent":["import { readFileSync, statSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, extname, join, resolve } from 'node:path';\nimport type ts from 'typescript';\n\n/**\n * Whether an export of a relative module is a Tasty component.\n *\n * Elements of a design-system component usually live next to it in `styled.ts`\n * or `elements.ts`, so a rule that only recognises `tasty()` results declared in\n * the file being linted misses most of them. This reads the imported module the\n * same syntactic way: an export is a component when it is a `tasty()` call (or a\n * configured options-kind factory), a `const` alias of one, an import from a\n * configured source, or a re-export that leads to one of those.\n *\n * Parsing goes through TypeScript, an optional peer dependency. Without it, an\n * imported component is simply not recognised, which is how it was before.\n */\nexport interface ModuleComponentOptions {\n importSources: ReadonlySet<string>;\n /** Imported names whose call result is a Tasty component. */\n isComponentFactory(importedName: string): boolean;\n}\n\ntype TypeScript = typeof ts;\n\ninterface ImportBinding {\n source: string;\n /** `'default'`, `'*'` for a namespace, or the exported name. */\n imported: string;\n}\n\ninterface ModuleInfo {\n imports: Map<string, ImportBinding>;\n constants: Map<string, ts.Expression>;\n /** Exported name -> local name. */\n exports: Map<string, string>;\n reexports: Map<string, ImportBinding>;\n starSources: string[];\n defaultExpression?: ts.Expression;\n}\n\nconst require = createRequire(import.meta.url);\nlet typescript: TypeScript | null | undefined;\nconst modules = new Map<string, { version: string; info: ModuleInfo }>();\n\nconst EXTENSIONS = [\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n];\n// A TypeScript ESM import names the emitted file: `./styled.js` is `styled.ts`.\nconst SOURCE_EXTENSIONS: Record<string, string[]> = {\n '.js': ['.ts', '.tsx'],\n '.jsx': ['.tsx'],\n '.mjs': ['.mts'],\n '.cjs': ['.cts'],\n};\n\nfunction getTypeScript(): TypeScript | null {\n if (typescript === undefined) {\n try {\n const module = require('typescript') as Partial<TypeScript>;\n // TypeScript 7 (the native port) exports only its version.\n typescript =\n typeof module.createSourceFile === 'function'\n ? (module as TypeScript)\n : null;\n } catch {\n typescript = null;\n }\n }\n return typescript;\n}\n\nfunction version(filename: string): string | null {\n try {\n const stat = statSync(filename);\n return stat.isFile() ? `${stat.mtimeMs}:${stat.size}` : null;\n } catch {\n return null;\n }\n}\n\n/** Resolve a relative specifier the way a bundler would, without `paths`. */\nexport function resolveRelativeModule(\n fromFile: string,\n specifier: string,\n): string | null {\n const base = resolve(dirname(fromFile), specifier);\n const extension = extname(base);\n const candidates = [\n ...(EXTENSIONS.includes(extension) ? [base] : []),\n ...(SOURCE_EXTENSIONS[extension] ?? []).map(\n (replacement) => base.slice(0, -extension.length) + replacement,\n ),\n ...EXTENSIONS.map((ext) => base + ext),\n ...EXTENSIONS.map((ext) => join(base, 'index' + ext)),\n ];\n\n return candidates.find((candidate) => version(candidate) !== null) ?? null;\n}\n\nfunction hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {\n const T = getTypeScript()!;\n return (\n T.canHaveModifiers(node) &&\n (T.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false)\n );\n}\n\nfunction readModule(filename: string): ModuleInfo | null {\n const T = getTypeScript();\n const current = version(filename);\n if (!T || current === null) return null;\n\n const cached = modules.get(filename);\n if (cached?.version === current) return cached.info;\n\n const source = T.createSourceFile(\n filename,\n readFileSync(filename, 'utf8'),\n T.ScriptTarget.Latest,\n );\n const info: ModuleInfo = {\n imports: new Map(),\n constants: new Map(),\n exports: new Map(),\n reexports: new Map(),\n starSources: [],\n };\n\n for (const statement of source.statements) {\n if (T.isImportDeclaration(statement)) {\n const clause = statement.importClause;\n if (!clause || clause.isTypeOnly) continue;\n if (!T.isStringLiteral(statement.moduleSpecifier)) continue;\n const from = statement.moduleSpecifier.text;\n if (clause.name) {\n info.imports.set(clause.name.text, {\n source: from,\n imported: 'default',\n });\n }\n const bindings = clause.namedBindings;\n if (bindings && T.isNamespaceImport(bindings)) {\n info.imports.set(bindings.name.text, { source: from, imported: '*' });\n } else if (bindings) {\n for (const element of bindings.elements) {\n if (element.isTypeOnly) continue;\n info.imports.set(element.name.text, {\n source: from,\n imported: (element.propertyName ?? element.name).text,\n });\n }\n }\n } else if (T.isVariableStatement(statement)) {\n if (!(statement.declarationList.flags & T.NodeFlags.Const)) continue;\n const exported = hasModifier(statement, T.SyntaxKind.ExportKeyword);\n for (const declaration of statement.declarationList.declarations) {\n if (!T.isIdentifier(declaration.name) || !declaration.initializer) {\n continue;\n }\n const name = declaration.name.text;\n info.constants.set(name, declaration.initializer);\n if (exported) info.exports.set(name, name);\n }\n } else if (T.isExportDeclaration(statement)) {\n if (statement.isTypeOnly) continue;\n const from =\n statement.moduleSpecifier &&\n T.isStringLiteral(statement.moduleSpecifier)\n ? statement.moduleSpecifier.text\n : null;\n const clause = statement.exportClause;\n if (!clause) {\n if (from !== null) info.starSources.push(from);\n } else if (T.isNamedExports(clause)) {\n for (const element of clause.elements) {\n if (element.isTypeOnly) continue;\n const local = (element.propertyName ?? element.name).text;\n if (from === null) info.exports.set(element.name.text, local);\n else\n info.reexports.set(element.name.text, {\n source: from,\n imported: local,\n });\n }\n }\n } else if (T.isExportAssignment(statement) && !statement.isExportEquals) {\n info.defaultExpression = statement.expression;\n }\n }\n\n modules.set(filename, { version: current, info });\n return info;\n}\n\n/** Whether `exportName` of the module `specifier` (relative to `fromFile`) is a Tasty component. */\nexport function isModuleTastyComponent(\n fromFile: string,\n specifier: string,\n exportName: string,\n options: ModuleComponentOptions,\n visited = new Set<string>(),\n): boolean {\n if (!specifier.startsWith('.')) return false;\n const filename = resolveRelativeModule(fromFile, specifier);\n if (filename === null) return false;\n\n const key = `${filename}\\0${exportName}`;\n if (visited.has(key)) return false;\n visited.add(key);\n\n const info = readModule(filename);\n if (!info) return false;\n const T = getTypeScript()!;\n\n const fromImport = (binding: ImportBinding): boolean =>\n options.importSources.has(binding.source) ||\n (binding.imported !== '*' &&\n isModuleTastyComponent(\n filename,\n binding.source,\n binding.imported,\n options,\n visited,\n ));\n\n const seenLocals = new Set<string>();\n const fromLocal = (name: string): boolean => {\n if (seenLocals.has(name)) return false;\n seenLocals.add(name);\n const init = info.constants.get(name);\n if (init) return fromExpression(init);\n const binding = info.imports.get(name);\n return binding !== undefined && fromImport(binding);\n };\n\n const fromExpression = (node: ts.Expression): boolean => {\n while (\n T.isParenthesizedExpression(node) ||\n T.isAsExpression(node) ||\n T.isSatisfiesExpression(node) ||\n T.isNonNullExpression(node) ||\n T.isTypeAssertionExpression(node)\n ) {\n node = node.expression;\n }\n if (T.isIdentifier(node)) return fromLocal(node.text);\n if (T.isPropertyAccessExpression(node)) {\n // `UI.Panel` from `import * as UI from './ui'` is that module's `Panel`.\n if (T.isIdentifier(node.expression)) {\n const binding = info.imports.get(node.expression.text);\n if (\n binding?.imported === '*' &&\n !options.importSources.has(binding.source)\n ) {\n return isModuleTastyComponent(\n filename,\n binding.source,\n node.name.text,\n options,\n visited,\n );\n }\n }\n let root: ts.Expression = node;\n while (T.isPropertyAccessExpression(root)) root = root.expression;\n return T.isIdentifier(root) && fromLocal(root.text);\n }\n if (T.isCallExpression(node) && T.isIdentifier(node.expression)) {\n const binding = info.imports.get(node.expression.text);\n return (\n binding !== undefined &&\n options.importSources.has(binding.source) &&\n options.isComponentFactory(binding.imported)\n );\n }\n return false;\n };\n\n const local = info.exports.get(exportName);\n if (local !== undefined) return fromLocal(local);\n if (exportName === 'default' && info.defaultExpression) {\n return fromExpression(info.defaultExpression);\n }\n const reexport = info.reexports.get(exportName);\n if (reexport) return fromImport(reexport);\n if (exportName === 'default') return false;\n\n return info.starSources.some((source) =>\n isModuleTastyComponent(filename, source, exportName, options, visited),\n );\n}\n\n/** The package import an export of a relative module comes from. */\nexport interface ModuleExportOrigin {\n /** The package specifier, as written in the import. */\n source: string;\n /** The name the package exports it under. */\n name: string;\n /** Members read off that export: `['Group']` for `Button.Group`. */\n members: string[];\n /** Whether a component factory call, such as `tasty(Button)`, wraps it. */\n wrapped: boolean;\n}\n\n/**\n * An origin with more members read off it. A factory wrapper's members are its\n * own sub-elements, not the wrapped component's, so they lead nowhere.\n */\nexport function withMembers(\n origin: ModuleExportOrigin | null,\n members: string[],\n): ModuleExportOrigin | null {\n if (!origin || members.length === 0) return origin;\n if (origin.wrapped) return null;\n return { ...origin, members: [...origin.members, ...members] };\n}\n\nexport interface ModuleOriginOptions extends ModuleComponentOptions {\n /** Whether `export * from source` may provide the name being followed. */\n isOriginSource(source: string): boolean;\n}\n\n/**\n * Follow `exportName` of the module `specifier` (relative to `fromFile`) to the\n * package import it comes from: through re-exports, `const` aliases, namespace\n * members (`UI.Button` is `Button`), member reads (`Button.Group`), and factory\n * wrappers whose first argument is the component they extend.\n */\nexport function resolveModuleExportOrigin(\n fromFile: string,\n specifier: string,\n exportName: string,\n options: ModuleOriginOptions,\n visited = new Set<string>(),\n): ModuleExportOrigin | null {\n if (!specifier.startsWith('.')) return null;\n const filename = resolveRelativeModule(fromFile, specifier);\n if (filename === null) return null;\n\n const key = `${filename}\\0${exportName}`;\n if (visited.has(key)) return null;\n visited.add(key);\n\n const info = readModule(filename);\n if (!info) return null;\n const T = getTypeScript()!;\n\n /** `members` are read off the binding, so a namespace's first one is the export. */\n const fromImport = (\n binding: ImportBinding,\n members: string[] = [],\n ): ModuleExportOrigin | null => {\n const namespace = binding.imported === '*';\n const name = namespace ? members[0] : binding.imported;\n const rest = namespace ? members.slice(1) : members;\n if (name === undefined) return null;\n if (!binding.source.startsWith('.')) {\n return { source: binding.source, name, members: rest, wrapped: false };\n }\n return withMembers(\n resolveModuleExportOrigin(\n filename,\n binding.source,\n name,\n options,\n visited,\n ),\n rest,\n );\n };\n\n const seenLocals = new Set<string>();\n const fromLocal = (\n name: string,\n members: string[] = [],\n ): ModuleExportOrigin | null => {\n const seen = [name, ...members].join('.');\n if (seenLocals.has(seen)) return null;\n seenLocals.add(seen);\n const init = info.constants.get(name);\n if (init) return fromExpression(init, members);\n const binding = info.imports.get(name);\n return binding ? fromImport(binding, members) : null;\n };\n\n const fromExpression = (\n node: ts.Expression,\n members: string[] = [],\n ): ModuleExportOrigin | null => {\n while (\n T.isParenthesizedExpression(node) ||\n T.isAsExpression(node) ||\n T.isSatisfiesExpression(node) ||\n T.isNonNullExpression(node) ||\n T.isTypeAssertionExpression(node)\n ) {\n node = node.expression;\n }\n if (T.isIdentifier(node)) return fromLocal(node.text, members);\n if (T.isPropertyAccessExpression(node)) {\n let root: ts.Expression = node;\n const path = [...members];\n while (T.isPropertyAccessExpression(root)) {\n path.unshift(root.name.text);\n root = root.expression;\n }\n return T.isIdentifier(root) ? fromLocal(root.text, path) : null;\n }\n if (\n members.length === 0 &&\n T.isCallExpression(node) &&\n T.isIdentifier(node.expression)\n ) {\n const binding = info.imports.get(node.expression.text);\n const base = node.arguments[0];\n if (\n !binding ||\n !options.importSources.has(binding.source) ||\n !options.isComponentFactory(binding.imported) ||\n !base ||\n T.isObjectLiteralExpression(base)\n ) {\n return null;\n }\n const origin = fromExpression(base);\n return origin && { ...origin, wrapped: true };\n }\n return null;\n };\n\n const local = info.exports.get(exportName);\n if (local !== undefined) return fromLocal(local);\n if (exportName === 'default' && info.defaultExpression) {\n return fromExpression(info.defaultExpression);\n }\n const reexport = info.reexports.get(exportName);\n if (reexport) return fromImport(reexport);\n if (exportName === 'default') return null;\n\n for (const source of info.starSources) {\n const origin = source.startsWith('.')\n ? resolveModuleExportOrigin(\n filename,\n source,\n exportName,\n options,\n visited,\n )\n : options.isOriginSource(source)\n ? { source, name: exportName, members: [], wrapped: false }\n : null;\n if (origin) return origin;\n }\n return null;\n}\n"],"mappings":"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"}
@@ -1,8 +1,14 @@
1
1
  import { DIMENSION_STYLES, POSITION_STYLES } from "./constants.js";
2
2
  import { unwrapExpression, wildcardPattern } from "./utils.js";
3
- import { resolveModuleExportOrigin } from "./module-components.js";
3
+ import { resolveModuleExportOrigin, withMembers } from "./module-components.js";
4
4
 
5
5
  //#region src/protected-components.ts
6
+ function namePattern(pattern) {
7
+ return {
8
+ regex: wildcardPattern(pattern),
9
+ member: pattern.includes(".")
10
+ };
11
+ }
6
12
  const STYLE_PROP_GROUPS = new Map([["position", POSITION_STYLES], ["dimension", DIMENSION_STYLES]]);
7
13
  const entriesByConfig = /* @__PURE__ */ new WeakMap();
8
14
  function readEntries(config) {
@@ -22,8 +28,8 @@ function readEntries(config) {
22
28
  };
23
29
  return {
24
30
  source: wildcardPattern(entry.source),
25
- include: strings("include").map(wildcardPattern),
26
- exclude: strings("exclude").map(wildcardPattern),
31
+ include: strings("include").map(namePattern),
32
+ exclude: strings("exclude").map(namePattern),
27
33
  allowedStyleProps: new Set(strings("allowedStyleProps").flatMap((name) => STYLE_PROP_GROUPS.get(name) ?? [name]))
28
34
  };
29
35
  });
@@ -35,10 +41,10 @@ function importedName(specifier) {
35
41
  }
36
42
  /**
37
43
  * Resolves JSX tags and `tasty()` bases to the `protectedComponents` entry they
38
- * name, by the name their source exports them under. Aliases, namespace members
39
- * (`UI.Button`, and `Text.Strong` as `Text`), `const` aliases, `tasty()`
40
- * wrappers and relative re-exports are followed; relative modules need the
41
- * `typescript` package, as for other cross-file recognition.
44
+ * name, by the name their source exports them under plus any members read off
45
+ * it (`Button.Group`). Aliases, namespace members (`UI.Button`), `const`
46
+ * aliases, `tasty()` wrappers and relative re-exports are followed; relative
47
+ * modules need the `typescript` package, as for other cross-file recognition.
42
48
  */
43
49
  var ProtectedComponents = class {
44
50
  entries;
@@ -61,41 +67,61 @@ var ProtectedComponents = class {
61
67
  resolveTag(name) {
62
68
  if (name.type === "JSXIdentifier" && !/^[A-Z]/.test(name.name)) return null;
63
69
  let root = name;
64
- let member;
70
+ const members = [];
65
71
  while (root.type === "JSXMemberExpression") {
66
- member = root.property.name;
72
+ members.unshift(root.property.name);
67
73
  root = root.object;
68
74
  }
69
75
  if (root.type !== "JSXIdentifier") return null;
70
- return this.match(this.resolveBinding(root, root.name, member, /* @__PURE__ */ new Set()));
76
+ return this.match(this.resolveBinding(root, root.name, members, /* @__PURE__ */ new Set()));
71
77
  }
72
78
  /** The protected component an expression, such as a `tasty()` base, refers to. */
73
79
  resolveExpression(node) {
74
- return this.match(this.origin(node, void 0, /* @__PURE__ */ new Set()));
80
+ return this.match(this.origin(node, [], /* @__PURE__ */ new Set()));
75
81
  }
82
+ /**
83
+ * The most specific name decides: an entry that names a member path
84
+ * (`Button.Group`, `Button.*`) in `include` or `exclude` wins over one that
85
+ * matches only the export, so a sub-component that is a separate component
86
+ * can follow its own entry. Longer paths are tried first.
87
+ */
76
88
  match(origin) {
77
89
  if (!origin) return null;
78
- const entry = this.entries.find((candidate) => candidate.source.test(origin.source) && candidate.include.some((pattern) => pattern.test(origin.name)) && !candidate.exclude.some((pattern) => pattern.test(origin.name)));
79
- return entry ? {
80
- ...origin,
90
+ const entries = this.entries.filter((entry) => entry.source.test(origin.source));
91
+ const protect = (name, entry) => ({
92
+ name,
93
+ source: origin.source,
94
+ wrapped: origin.wrapped,
81
95
  allowedStyleProps: entry.allowedStyleProps
82
- } : null;
96
+ });
97
+ const parts = [origin.name, ...origin.members];
98
+ for (let length = parts.length; length > 1; length--) {
99
+ const path = parts.slice(0, length).join(".");
100
+ const names = (patterns) => patterns.some((pattern) => pattern.member && pattern.regex.test(path));
101
+ for (const entry of entries) {
102
+ if (names(entry.exclude)) return null;
103
+ if (names(entry.include)) return protect(path, entry);
104
+ }
105
+ }
106
+ const matches = (patterns) => patterns.some((pattern) => !pattern.member && pattern.regex.test(origin.name));
107
+ const entry = entries.find((candidate) => matches(candidate.include) && !matches(candidate.exclude));
108
+ return entry ? protect(origin.name, entry) : null;
83
109
  }
84
- origin(node, member, visited) {
110
+ origin(node, members, visited) {
85
111
  const value = unwrapExpression(node);
86
- if (value.type === "Identifier") return this.resolveBinding(value, value.name, member, visited);
112
+ if (value.type === "Identifier") return this.resolveBinding(value, value.name, members, visited);
87
113
  if (value.type === "MemberExpression") {
88
114
  let root = value;
89
- let first = member;
115
+ const path = [...members];
90
116
  while (root.type === "MemberExpression") {
91
117
  if (root.computed || root.property.type !== "Identifier") return null;
92
- first = root.property.name;
118
+ path.unshift(root.property.name);
93
119
  root = unwrapExpression(root.object);
94
120
  }
95
- return root.type === "Identifier" ? this.resolveBinding(root, root.name, first, visited) : null;
121
+ return root.type === "Identifier" ? this.resolveBinding(root, root.name, path, visited) : null;
96
122
  }
97
- if (member === void 0 && value.type === "CallExpression" && this.isWrapperCall(value)) {
98
- const origin = this.origin(value.arguments[0], void 0, visited);
123
+ if (members.length === 0 && value.type === "CallExpression" && this.isWrapperCall(value)) {
124
+ const origin = this.origin(value.arguments[0], [], visited);
99
125
  return origin && {
100
126
  ...origin,
101
127
  wrapped: true
@@ -103,8 +129,8 @@ var ProtectedComponents = class {
103
129
  }
104
130
  return null;
105
131
  }
106
- /** `member` is the first property read off the binding, as in `UI.Button`. */
107
- resolveBinding(reference, localName, member, visited) {
132
+ /** `members` are the properties read off the binding, as in `UI.Button.Group`. */
133
+ resolveBinding(reference, localName, members, visited) {
108
134
  let scope = this.context.sourceCode.getScope(reference);
109
135
  while (scope && !scope.set.has(localName)) scope = scope.upper;
110
136
  const variable = scope?.set.get(localName);
@@ -114,18 +140,19 @@ var ProtectedComponents = class {
114
140
  cache = /* @__PURE__ */ new Map();
115
141
  this.bindings.set(variable, cache);
116
142
  }
117
- const key = member ?? "";
143
+ const key = members.join(".");
118
144
  if (cache.has(key)) return cache.get(key);
119
- const result = this.resolveDefinition(variable.defs[0], member, visited);
145
+ const result = this.resolveDefinition(variable.defs[0], members, visited);
120
146
  cache.set(key, result);
121
147
  return result;
122
148
  }
123
- resolveDefinition(def, member, visited) {
149
+ resolveDefinition(def, members, visited) {
124
150
  if (def.type === "ImportBinding") {
125
151
  if (def.parent.type !== "ImportDeclaration" || def.parent.importKind === "type") return null;
126
152
  const specifier = def.node;
127
153
  let name;
128
- if (specifier.type === "ImportNamespaceSpecifier") name = member;
154
+ let rest = members;
155
+ if (specifier.type === "ImportNamespaceSpecifier") [name, ...rest] = members;
129
156
  else if (specifier.type === "ImportDefaultSpecifier") name = "default";
130
157
  else if (specifier.type === "ImportSpecifier") {
131
158
  if (specifier.importKind === "type") return null;
@@ -136,13 +163,14 @@ var ProtectedComponents = class {
136
163
  if (!source.startsWith(".")) return {
137
164
  source,
138
165
  name,
166
+ members: rest,
139
167
  wrapped: false
140
168
  };
141
- return this.moduleOrigin(source, name);
169
+ return withMembers(this.moduleOrigin(source, name), rest);
142
170
  }
143
171
  if (def.type !== "Variable" || def.parent.kind !== "const" || def.node.id.type !== "Identifier" || !def.node.init || visited.has(def.node)) return null;
144
172
  visited.add(def.node);
145
- return this.origin(def.node.init, member, visited);
173
+ return this.origin(def.node.init, members, visited);
146
174
  }
147
175
  moduleOrigin(source, exportName) {
148
176
  const key = `${source}\0${exportName}`;
@@ -1 +1 @@
1
- {"version":3,"file":"protected-components.js","names":[],"sources":["../src/protected-components.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext, Scope } from '@typescript-eslint/utils/ts-eslint';\nimport type { TastyContext } from './context.js';\nimport type { ResolvedConfig } from './types.js';\nimport { DIMENSION_STYLES, POSITION_STYLES } from './constants.js';\nimport {\n type ModuleExportOrigin,\n resolveModuleExportOrigin,\n} from './module-components.js';\nimport { unwrapExpression, wildcardPattern } from './utils.js';\n\n/** A component resolved to a `protectedComponents` entry. */\nexport interface ProtectedComponent {\n /** The name its source exports it under: `Button` for `<UI.Button>` or an alias. */\n name: string;\n source: string;\n /** Whether a `tasty()` wrapper sits between the reference and the export. */\n wrapped: boolean;\n allowedStyleProps: ReadonlySet<string>;\n}\n\ninterface Entry {\n source: RegExp;\n include: RegExp[];\n exclude: RegExp[];\n allowedStyleProps: ReadonlySet<string>;\n}\n\nconst STYLE_PROP_GROUPS = new Map<string, readonly string[]>([\n ['position', POSITION_STYLES],\n ['dimension', DIMENSION_STYLES],\n]);\n\nconst entriesByConfig = new WeakMap<ResolvedConfig, Entry[]>();\n\nfunction readEntries(config: ResolvedConfig): Entry[] {\n let entries = entriesByConfig.get(config);\n if (entries) return entries;\n\n const list: unknown = config.protectedComponents ?? [];\n if (!Array.isArray(list)) {\n throw new Error('tasty: protectedComponents must be an array.');\n }\n entries = list.map((item: unknown, index): Entry => {\n const at = `tasty: protectedComponents[${index}]`;\n if (\n !item ||\n typeof item !== 'object' ||\n typeof (item as { source?: unknown }).source !== 'string'\n ) {\n throw new Error(`${at} must be an object with a string 'source'.`);\n }\n const entry = item as Record<string, unknown>;\n const strings = (key: string): string[] => {\n const value = entry[key];\n if (value === undefined) return key === 'include' ? ['*'] : [];\n if (\n !Array.isArray(value) ||\n value.some((name) => typeof name !== 'string')\n ) {\n throw new Error(`${at}.${key} must be an array of strings.`);\n }\n return value as string[];\n };\n return {\n source: wildcardPattern(entry.source as string),\n include: strings('include').map(wildcardPattern),\n exclude: strings('exclude').map(wildcardPattern),\n allowedStyleProps: new Set(\n strings('allowedStyleProps').flatMap(\n (name) => STYLE_PROP_GROUPS.get(name) ?? [name],\n ),\n ),\n };\n });\n entriesByConfig.set(config, entries);\n return entries;\n}\n\nfunction importedName(specifier: TSESTree.ImportSpecifier): string {\n return specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : specifier.imported.value;\n}\n\n/**\n * Resolves JSX tags and `tasty()` bases to the `protectedComponents` entry they\n * name, by the name their source exports them under. Aliases, namespace members\n * (`UI.Button`, and `Text.Strong` as `Text`), `const` aliases, `tasty()`\n * wrappers and relative re-exports are followed; relative modules need the\n * `typescript` package, as for other cross-file recognition.\n */\nexport class ProtectedComponents {\n private entries: Entry[];\n private bindings = new WeakMap<\n Scope.Variable,\n Map<string, ModuleExportOrigin | null>\n >();\n private modules = new Map<string, ModuleExportOrigin | null>();\n\n constructor(\n private context: RuleContext<string, unknown[]>,\n private ctx: TastyContext,\n ) {\n this.entries = readEntries(ctx.config);\n }\n\n get enabled(): boolean {\n return this.entries.length > 0;\n }\n\n /** `tasty(Base, ...)`: a Tasty wrapper of an existing component. */\n isWrapperCall(node: TSESTree.CallExpression): boolean {\n const base = node.arguments[0];\n return (\n this.ctx.isTastyCall(node)?.importedName === 'tasty' &&\n base !== undefined &&\n base.type !== 'SpreadElement' &&\n unwrapExpression(base).type !== 'ObjectExpression'\n );\n }\n\n /** The protected component a JSX tag name refers to. */\n resolveTag(name: TSESTree.JSXTagNameExpression): ProtectedComponent | null {\n // `<button>` is a native element whatever a `button` binding holds.\n if (name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name)) return null;\n let root = name;\n let member: string | undefined;\n while (root.type === 'JSXMemberExpression') {\n member = root.property.name;\n root = root.object;\n }\n if (root.type !== 'JSXIdentifier') return null;\n return this.match(this.resolveBinding(root, root.name, member, new Set()));\n }\n\n /** The protected component an expression, such as a `tasty()` base, refers to. */\n resolveExpression(node: TSESTree.Node): ProtectedComponent | null {\n return this.match(this.origin(node, undefined, new Set()));\n }\n\n private match(origin: ModuleExportOrigin | null): ProtectedComponent | null {\n if (!origin) return null;\n const entry = this.entries.find(\n (candidate) =>\n candidate.source.test(origin.source) &&\n candidate.include.some((pattern) => pattern.test(origin.name)) &&\n !candidate.exclude.some((pattern) => pattern.test(origin.name)),\n );\n return entry\n ? { ...origin, allowedStyleProps: entry.allowedStyleProps }\n : null;\n }\n\n private origin(\n node: TSESTree.Node,\n member: string | undefined,\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n const value = unwrapExpression(node);\n if (value.type === 'Identifier') {\n return this.resolveBinding(value, value.name, member, visited);\n }\n if (value.type === 'MemberExpression') {\n let root: TSESTree.Node = value;\n let first = member;\n while (root.type === 'MemberExpression') {\n if (root.computed || root.property.type !== 'Identifier') return null;\n first = root.property.name;\n root = unwrapExpression(root.object);\n }\n return root.type === 'Identifier'\n ? this.resolveBinding(root, root.name, first, visited)\n : null;\n }\n if (\n member === undefined &&\n value.type === 'CallExpression' &&\n this.isWrapperCall(value)\n ) {\n const origin = this.origin(value.arguments[0], undefined, visited);\n return origin && { ...origin, wrapped: true };\n }\n return null;\n }\n\n /** `member` is the first property read off the binding, as in `UI.Button`. */\n private resolveBinding(\n reference: TSESTree.Node,\n localName: string,\n member: string | undefined,\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n let scope: Scope.Scope | null = this.context.sourceCode.getScope(reference);\n while (scope && !scope.set.has(localName)) scope = scope.upper;\n const variable = scope?.set.get(localName);\n if (!variable || variable.defs.length !== 1) return null;\n\n let cache = this.bindings.get(variable);\n if (!cache) {\n cache = new Map();\n this.bindings.set(variable, cache);\n }\n const key = member ?? '';\n if (cache.has(key)) return cache.get(key)!;\n\n const result = this.resolveDefinition(variable.defs[0], member, visited);\n cache.set(key, result);\n return result;\n }\n\n private resolveDefinition(\n def: Scope.Definition,\n member: string | undefined,\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n if (def.type === 'ImportBinding') {\n if (\n def.parent.type !== 'ImportDeclaration' ||\n def.parent.importKind === 'type'\n ) {\n return null;\n }\n const specifier = def.node;\n let name: string | undefined;\n if (specifier.type === 'ImportNamespaceSpecifier') name = member;\n else if (specifier.type === 'ImportDefaultSpecifier') name = 'default';\n else if (specifier.type === 'ImportSpecifier') {\n if (specifier.importKind === 'type') return null;\n name = importedName(specifier);\n }\n if (name === undefined) return null;\n\n const source = def.parent.source.value;\n if (!source.startsWith('.')) return { source, name, wrapped: false };\n return this.moduleOrigin(source, name);\n }\n\n if (\n def.type !== 'Variable' ||\n def.parent.kind !== 'const' ||\n def.node.id.type !== 'Identifier' ||\n !def.node.init ||\n visited.has(def.node)\n ) {\n return null;\n }\n visited.add(def.node);\n return this.origin(def.node.init, member, visited);\n }\n\n private moduleOrigin(\n source: string,\n exportName: string,\n ): ModuleExportOrigin | null {\n const key = `${source}\\0${exportName}`;\n if (!this.modules.has(key)) {\n this.modules.set(\n key,\n resolveModuleExportOrigin(this.context.filename, source, exportName, {\n importSources: this.ctx.trackedSources,\n isComponentFactory: (name) => name === 'tasty',\n isOriginSource: (candidate) =>\n this.entries.some((entry) => entry.source.test(candidate)),\n }),\n );\n }\n return this.modules.get(key)!;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"protected-components.js","names":[],"sources":["../src/protected-components.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext, Scope } from '@typescript-eslint/utils/ts-eslint';\nimport type { TastyContext } from './context.js';\nimport type { ResolvedConfig } from './types.js';\nimport { DIMENSION_STYLES, POSITION_STYLES } from './constants.js';\nimport {\n type ModuleExportOrigin,\n resolveModuleExportOrigin,\n withMembers,\n} from './module-components.js';\nimport { unwrapExpression, wildcardPattern } from './utils.js';\n\n/** A component resolved to a `protectedComponents` entry. */\nexport interface ProtectedComponent {\n /**\n * The name it is protected under: the export name (`Button` for `<UI.Button>`,\n * an alias or `<Button.Split>`), or the member path an entry names, such as\n * `Button.Group`.\n */\n name: string;\n source: string;\n /** Whether a `tasty()` wrapper sits between the reference and the export. */\n wrapped: boolean;\n allowedStyleProps: ReadonlySet<string>;\n}\n\ninterface Pattern {\n regex: RegExp;\n /** Names a member path, such as `Button.Group` or `Button.*`. */\n member: boolean;\n}\n\ninterface Entry {\n source: RegExp;\n include: Pattern[];\n exclude: Pattern[];\n allowedStyleProps: ReadonlySet<string>;\n}\n\nfunction namePattern(pattern: string): Pattern {\n return { regex: wildcardPattern(pattern), member: pattern.includes('.') };\n}\n\nconst STYLE_PROP_GROUPS = new Map<string, readonly string[]>([\n ['position', POSITION_STYLES],\n ['dimension', DIMENSION_STYLES],\n]);\n\nconst entriesByConfig = new WeakMap<ResolvedConfig, Entry[]>();\n\nfunction readEntries(config: ResolvedConfig): Entry[] {\n let entries = entriesByConfig.get(config);\n if (entries) return entries;\n\n const list: unknown = config.protectedComponents ?? [];\n if (!Array.isArray(list)) {\n throw new Error('tasty: protectedComponents must be an array.');\n }\n entries = list.map((item: unknown, index): Entry => {\n const at = `tasty: protectedComponents[${index}]`;\n if (\n !item ||\n typeof item !== 'object' ||\n typeof (item as { source?: unknown }).source !== 'string'\n ) {\n throw new Error(`${at} must be an object with a string 'source'.`);\n }\n const entry = item as Record<string, unknown>;\n const strings = (key: string): string[] => {\n const value = entry[key];\n if (value === undefined) return key === 'include' ? ['*'] : [];\n if (\n !Array.isArray(value) ||\n value.some((name) => typeof name !== 'string')\n ) {\n throw new Error(`${at}.${key} must be an array of strings.`);\n }\n return value as string[];\n };\n return {\n source: wildcardPattern(entry.source as string),\n include: strings('include').map(namePattern),\n exclude: strings('exclude').map(namePattern),\n allowedStyleProps: new Set(\n strings('allowedStyleProps').flatMap(\n (name) => STYLE_PROP_GROUPS.get(name) ?? [name],\n ),\n ),\n };\n });\n entriesByConfig.set(config, entries);\n return entries;\n}\n\nfunction importedName(specifier: TSESTree.ImportSpecifier): string {\n return specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : specifier.imported.value;\n}\n\n/**\n * Resolves JSX tags and `tasty()` bases to the `protectedComponents` entry they\n * name, by the name their source exports them under plus any members read off\n * it (`Button.Group`). Aliases, namespace members (`UI.Button`), `const`\n * aliases, `tasty()` wrappers and relative re-exports are followed; relative\n * modules need the `typescript` package, as for other cross-file recognition.\n */\nexport class ProtectedComponents {\n private entries: Entry[];\n private bindings = new WeakMap<\n Scope.Variable,\n Map<string, ModuleExportOrigin | null>\n >();\n private modules = new Map<string, ModuleExportOrigin | null>();\n\n constructor(\n private context: RuleContext<string, unknown[]>,\n private ctx: TastyContext,\n ) {\n this.entries = readEntries(ctx.config);\n }\n\n get enabled(): boolean {\n return this.entries.length > 0;\n }\n\n /** `tasty(Base, ...)`: a Tasty wrapper of an existing component. */\n isWrapperCall(node: TSESTree.CallExpression): boolean {\n const base = node.arguments[0];\n return (\n this.ctx.isTastyCall(node)?.importedName === 'tasty' &&\n base !== undefined &&\n base.type !== 'SpreadElement' &&\n unwrapExpression(base).type !== 'ObjectExpression'\n );\n }\n\n /** The protected component a JSX tag name refers to. */\n resolveTag(name: TSESTree.JSXTagNameExpression): ProtectedComponent | null {\n // `<button>` is a native element whatever a `button` binding holds.\n if (name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name)) return null;\n let root = name;\n const members: string[] = [];\n while (root.type === 'JSXMemberExpression') {\n members.unshift(root.property.name);\n root = root.object;\n }\n if (root.type !== 'JSXIdentifier') return null;\n return this.match(this.resolveBinding(root, root.name, members, new Set()));\n }\n\n /** The protected component an expression, such as a `tasty()` base, refers to. */\n resolveExpression(node: TSESTree.Node): ProtectedComponent | null {\n return this.match(this.origin(node, [], new Set()));\n }\n\n /**\n * The most specific name decides: an entry that names a member path\n * (`Button.Group`, `Button.*`) in `include` or `exclude` wins over one that\n * matches only the export, so a sub-component that is a separate component\n * can follow its own entry. Longer paths are tried first.\n */\n private match(origin: ModuleExportOrigin | null): ProtectedComponent | null {\n if (!origin) return null;\n const entries = this.entries.filter((entry) =>\n entry.source.test(origin.source),\n );\n const protect = (name: string, entry: Entry): ProtectedComponent => ({\n name,\n source: origin.source,\n wrapped: origin.wrapped,\n allowedStyleProps: entry.allowedStyleProps,\n });\n\n const parts = [origin.name, ...origin.members];\n for (let length = parts.length; length > 1; length--) {\n const path = parts.slice(0, length).join('.');\n const names = (patterns: Pattern[]) =>\n patterns.some((pattern) => pattern.member && pattern.regex.test(path));\n for (const entry of entries) {\n if (names(entry.exclude)) return null;\n if (names(entry.include)) return protect(path, entry);\n }\n }\n\n const matches = (patterns: Pattern[]) =>\n patterns.some(\n (pattern) => !pattern.member && pattern.regex.test(origin.name),\n );\n const entry = entries.find(\n (candidate) => matches(candidate.include) && !matches(candidate.exclude),\n );\n return entry ? protect(origin.name, entry) : null;\n }\n\n private origin(\n node: TSESTree.Node,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n const value = unwrapExpression(node);\n if (value.type === 'Identifier') {\n return this.resolveBinding(value, value.name, members, visited);\n }\n if (value.type === 'MemberExpression') {\n let root: TSESTree.Node = value;\n const path = [...members];\n while (root.type === 'MemberExpression') {\n if (root.computed || root.property.type !== 'Identifier') return null;\n path.unshift(root.property.name);\n root = unwrapExpression(root.object);\n }\n return root.type === 'Identifier'\n ? this.resolveBinding(root, root.name, path, visited)\n : null;\n }\n if (\n members.length === 0 &&\n value.type === 'CallExpression' &&\n this.isWrapperCall(value)\n ) {\n const origin = this.origin(value.arguments[0], [], visited);\n return origin && { ...origin, wrapped: true };\n }\n return null;\n }\n\n /** `members` are the properties read off the binding, as in `UI.Button.Group`. */\n private resolveBinding(\n reference: TSESTree.Node,\n localName: string,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n let scope: Scope.Scope | null = this.context.sourceCode.getScope(reference);\n while (scope && !scope.set.has(localName)) scope = scope.upper;\n const variable = scope?.set.get(localName);\n if (!variable || variable.defs.length !== 1) return null;\n\n let cache = this.bindings.get(variable);\n if (!cache) {\n cache = new Map();\n this.bindings.set(variable, cache);\n }\n const key = members.join('.');\n if (cache.has(key)) return cache.get(key)!;\n\n const result = this.resolveDefinition(variable.defs[0], members, visited);\n cache.set(key, result);\n return result;\n }\n\n private resolveDefinition(\n def: Scope.Definition,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n if (def.type === 'ImportBinding') {\n if (\n def.parent.type !== 'ImportDeclaration' ||\n def.parent.importKind === 'type'\n ) {\n return null;\n }\n const specifier = def.node;\n let name: string | undefined;\n let rest = members;\n if (specifier.type === 'ImportNamespaceSpecifier') {\n [name, ...rest] = members;\n } else if (specifier.type === 'ImportDefaultSpecifier') name = 'default';\n else if (specifier.type === 'ImportSpecifier') {\n if (specifier.importKind === 'type') return null;\n name = importedName(specifier);\n }\n if (name === undefined) return null;\n\n const source = def.parent.source.value;\n if (!source.startsWith('.')) {\n return { source, name, members: rest, wrapped: false };\n }\n return withMembers(this.moduleOrigin(source, name), rest);\n }\n\n if (\n def.type !== 'Variable' ||\n def.parent.kind !== 'const' ||\n def.node.id.type !== 'Identifier' ||\n !def.node.init ||\n visited.has(def.node)\n ) {\n return null;\n }\n visited.add(def.node);\n return this.origin(def.node.init, members, visited);\n }\n\n private moduleOrigin(\n source: string,\n exportName: string,\n ): ModuleExportOrigin | null {\n const key = `${source}\\0${exportName}`;\n if (!this.modules.has(key)) {\n this.modules.set(\n key,\n resolveModuleExportOrigin(this.context.filename, source, exportName, {\n importSources: this.ctx.trackedSources,\n isComponentFactory: (name) => name === 'tasty',\n isOriginSource: (candidate) =>\n this.entries.some((entry) => entry.source.test(candidate)),\n }),\n );\n }\n return this.modules.get(key)!;\n }\n}\n"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -12,9 +12,16 @@ interface StyleFunctionConfig {
12
12
  interface ProtectedComponentConfig {
13
13
  /** Package the components are imported from. `*` matches any run of characters. */
14
14
  source: string;
15
- /** Exported names to protect, `*` matching any run of characters. Defaults to every export. */
15
+ /**
16
+ * Exported names to protect, `*` matching any run of characters. Defaults to
17
+ * every export. A name with a dot is a member path, such as `Button.Split` or
18
+ * `Menu.*`, and wins over an entry that matches only the export.
19
+ */
16
20
  include?: string[];
17
- /** Exported names to leave out, such as layout and typography primitives. */
21
+ /**
22
+ * Exported names or member paths to leave out, such as layout and typography
23
+ * primitives, or a separate component attached as `Button.Group`.
24
+ */
18
25
  exclude?: string[];
19
26
  /**
20
27
  * Style props that place or size the root and stay allowed: `'position'` and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {