@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
|
|
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
|
-
|
|
187
|
-
|
|
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,
|
|
198
|
-
const seen =
|
|
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,
|
|
218
|
+
if (init) return fromExpression(init, members);
|
|
203
219
|
const binding = info.imports.get(name);
|
|
204
|
-
return binding ? fromImport(binding,
|
|
220
|
+
return binding ? fromImport(binding, members) : null;
|
|
205
221
|
};
|
|
206
|
-
const fromExpression = (node,
|
|
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,
|
|
224
|
+
if (T.isIdentifier(node)) return fromLocal(node.text, members);
|
|
209
225
|
if (T.isPropertyAccessExpression(node)) {
|
|
210
226
|
let root = node;
|
|
211
|
-
|
|
227
|
+
const path = [...members];
|
|
212
228
|
while (T.isPropertyAccessExpression(root)) {
|
|
213
|
-
|
|
229
|
+
path.unshift(root.name.text);
|
|
214
230
|
root = root.expression;
|
|
215
231
|
}
|
|
216
|
-
return T.isIdentifier(root) ? fromLocal(root.text,
|
|
232
|
+
return T.isIdentifier(root) ? fromLocal(root.text, path) : null;
|
|
217
233
|
}
|
|
218
|
-
if (
|
|
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(
|
|
26
|
-
exclude: strings("exclude").map(
|
|
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
|
|
39
|
-
* (`
|
|
40
|
-
* wrappers and relative re-exports are followed; relative
|
|
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
|
-
|
|
70
|
+
const members = [];
|
|
65
71
|
while (root.type === "JSXMemberExpression") {
|
|
66
|
-
|
|
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,
|
|
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,
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
}
|
|
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,
|
|
110
|
+
origin(node, members, visited) {
|
|
85
111
|
const value = unwrapExpression(node);
|
|
86
|
-
if (value.type === "Identifier") return this.resolveBinding(value, value.name,
|
|
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
|
-
|
|
115
|
+
const path = [...members];
|
|
90
116
|
while (root.type === "MemberExpression") {
|
|
91
117
|
if (root.computed || root.property.type !== "Identifier") return null;
|
|
92
|
-
|
|
118
|
+
path.unshift(root.property.name);
|
|
93
119
|
root = unwrapExpression(root.object);
|
|
94
120
|
}
|
|
95
|
-
return root.type === "Identifier" ? this.resolveBinding(root, root.name,
|
|
121
|
+
return root.type === "Identifier" ? this.resolveBinding(root, root.name, path, visited) : null;
|
|
96
122
|
}
|
|
97
|
-
if (
|
|
98
|
-
const origin = this.origin(value.arguments[0],
|
|
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
|
-
/** `
|
|
107
|
-
resolveBinding(reference, localName,
|
|
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 =
|
|
143
|
+
const key = members.join(".");
|
|
118
144
|
if (cache.has(key)) return cache.get(key);
|
|
119
|
-
const result = this.resolveDefinition(variable.defs[0],
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|