|
| 1 | +import { isStaticMember } from '@custom-elements-manifest/analyzer/src/utils/ast-helpers.js'; |
| 2 | +import { extractMixinNodes } from '@custom-elements-manifest/analyzer/src/utils/mixins.js'; |
| 3 | + |
1 | 4 | const inheritanceDenyList = ['PolylitMixin', 'DirMixin']; |
2 | 5 |
|
3 | 6 | // Attribute types that can't be set via HTML attributes |
@@ -131,12 +134,65 @@ function mixesPlugin() { |
131 | 134 | }; |
132 | 135 | } |
133 | 136 |
|
| 137 | +/** |
| 138 | + * CEM plugin that marks `readOnly: true` properties as `readonly` in custom-elements.json |
| 139 | + * to filter them out of Lit property bindings when generating web-types-lit.json files. |
| 140 | + */ |
| 141 | +function readonlyPlugin() { |
| 142 | + return { |
| 143 | + analyzePhase({ ts, node, moduleDoc }) { |
| 144 | + const mixinNodes = extractMixinNodes(node); |
| 145 | + const classNode = mixinNodes ? mixinNodes.mixinClass : ts.isClassDeclaration(node) ? node : undefined; |
| 146 | + if (!classNode) return; |
| 147 | + |
| 148 | + // Find `static get properties()` method |
| 149 | + for (const member of classNode.members || []) { |
| 150 | + if (!ts.isGetAccessorDeclaration(member) || !isStaticMember(member) || member.name?.text !== 'properties') { |
| 151 | + continue; |
| 152 | + } |
| 153 | + |
| 154 | + // Find the return statement's object literal |
| 155 | + const returnStmt = member.body?.statements?.find(ts.isReturnStatement); |
| 156 | + const returnObj = returnStmt?.expression; |
| 157 | + if (!returnObj || !ts.isObjectLiteralExpression(returnObj)) continue; |
| 158 | + |
| 159 | + for (const prop of returnObj.properties) { |
| 160 | + if (!ts.isPropertyAssignment(prop)) continue; |
| 161 | + const propName = prop.name?.text; |
| 162 | + if (!propName) continue; |
| 163 | + |
| 164 | + // Check if the property config object has `readOnly: true` |
| 165 | + const configObj = prop.initializer; |
| 166 | + if (!ts.isObjectLiteralExpression(configObj)) continue; |
| 167 | + |
| 168 | + const hasReadOnly = configObj.properties.some( |
| 169 | + (p) => |
| 170 | + ts.isPropertyAssignment(p) && |
| 171 | + p.name?.text === 'readOnly' && |
| 172 | + p.initializer?.kind === ts.SyntaxKind.TrueKeyword, |
| 173 | + ); |
| 174 | + if (!hasReadOnly) continue; |
| 175 | + |
| 176 | + // Find the matching member in moduleDoc declarations and mark it readonly |
| 177 | + for (const declaration of moduleDoc.declarations || []) { |
| 178 | + const cemMember = declaration.members?.find((m) => m.name === propName); |
| 179 | + if (cemMember) { |
| 180 | + cemMember.readonly = true; |
| 181 | + } |
| 182 | + } |
| 183 | + } |
| 184 | + } |
| 185 | + }, |
| 186 | + }; |
| 187 | +} |
| 188 | + |
134 | 189 | export default { |
135 | 190 | globs: ['packages/**/src/(vaadin-*.js|*-mixin.js)'], |
136 | 191 | packagejson: false, |
137 | 192 | litelement: true, |
138 | 193 | plugins: [ |
139 | 194 | mixesPlugin(), |
| 195 | + readonlyPlugin(), |
140 | 196 | { |
141 | 197 | packageLinkPhase({ customElementsManifest }) { |
142 | 198 | for (const definition of customElementsManifest.modules) { |
|
0 commit comments