Skip to content

Commit ebef10b

Browse files
committed
feat(cem): add support for @import JSDoc syntax
Add support for the modern @import JSDoc syntax alongside the existing @typedef syntax for importing types. Changes: - Add extractImportsFromImportTag() helper function to parse @import tags - Update moduleLinkPhase to process @import tags in addition to @typedef - Extract module paths and imported types from @import declarations - Use TypeScript's resolveModuleName() for consistent module resolution - Add comprehensive tests for @import functionality - Update existing tests to use new function signatures The @import syntax provides a cleaner alternative to the verbose @typedef {import('./path').Type} Type pattern: Before: @typedef {import('./types.js').Foo} Foo After: @import {Foo} from './types.js' Both syntaxes are now supported and work identically.
1 parent c2d623a commit ebef10b

File tree

4 files changed

+166
-21
lines changed

4 files changed

+166
-21
lines changed

cem/support-typedef-jsdoc-utils.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,36 @@ export function findTypePath(importTag, ts, moduleFilePath) {
159159
return null;
160160
}
161161

162+
export function extractImportsFromImportTag(importTag, ts, moduleFilePath) {
163+
// @import {Type1, Type2} from './path'
164+
// importTag.importClause.namedBindings.elements contains the types
165+
// importTag.moduleSpecifier.text contains the module path
166+
167+
const moduleSpecifier = importTag.moduleSpecifier?.text;
168+
const namedBindings = importTag.importClause?.namedBindings;
169+
170+
if (!moduleSpecifier || !namedBindings) {
171+
return null;
172+
}
173+
174+
// Use TypeScript's module resolution
175+
const compilerOptions = {
176+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
177+
allowJs: true,
178+
};
179+
180+
const result = ts.resolveModuleName(moduleSpecifier, moduleFilePath, compilerOptions, ts.sys);
181+
182+
if (!result.resolvedModule) {
183+
return null;
184+
}
185+
186+
const resolvedPath = result.resolvedModule.resolvedFileName;
187+
const types = namedBindings.elements?.map((el) => el.name?.getText()).filter(Boolean) || [];
188+
189+
return { path: resolvedPath, types };
190+
}
191+
162192
export function findSubtypes(ts, node, types, parents) {
163193
const subtypes = [];
164194

cem/support-typedef-jsdoc.js

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
22
import ts from 'typescript';
33
import {
44
convertInterface,
5+
extractImportsFromImportTag,
56
findInterfacesFromExtends,
67
findPathAndTypesFromImports,
78
findSubtypes,
@@ -133,34 +134,68 @@ export default function supportTypedefJsdoc() {
133134
return;
134135
}
135136

136-
// This finds the comment where the imports are located
137+
// This finds the comment where the @typedef imports are located
137138
const typeDefNode = statement?.jsDoc?.filter((statement) =>
138139
statement.tags?.find((tag) => tag.kind === ts.SyntaxKind.JSDocTypedefTag),
139140
)?.[0];
140141

141-
// Check the jsDoc of the class and find the imports
142-
typeDefNode?.tags?.forEach((tag) => {
143-
// Extract the path from the @typedef import
144-
const typePath = findTypePath(tag, ts, moduleDoc.path);
142+
// Check the jsDoc of the class and find the @typedef imports
143+
typeDefNode?.tags
144+
?.filter((tag) => tag.kind === ts.SyntaxKind.JSDocTypedefTag)
145+
.forEach((tag) => {
146+
// Extract the path from the @typedef import
147+
const typePath = findTypePath(tag, ts, moduleDoc.path);
148+
149+
// If an import is not correct, warn the plugin user.
150+
if (typePath == null) {
151+
console.warn(`[${componentName}] - There's a problem with one of your @typedef - ${tag.getText()}`);
152+
process.exitCode = 1;
153+
return;
154+
}
155+
156+
// Extract the type from the @typedef import
157+
const typeDefDisplay = tag.name.getText();
158+
159+
const type = types.find((type) => type === typeDefDisplay);
160+
161+
if (type != null) {
162+
if (!moduleTypeCache.has(typePath)) {
163+
moduleTypeCache.set(typePath, new Set());
164+
}
165+
moduleTypeCache.get(typePath).add(type);
166+
}
167+
});
168+
169+
// Find all @import tags
170+
const importTags =
171+
statement?.jsDoc?.flatMap(
172+
(doc) => doc.tags?.filter((tag) => tag.kind === ts.SyntaxKind.JSDocImportTag) || [],
173+
) || [];
174+
175+
// Process @import tags
176+
importTags.forEach((tag) => {
177+
const importData = extractImportsFromImportTag(tag, ts, moduleDoc.path);
145178

146179
// If an import is not correct, warn the plugin user.
147-
if (typePath == null) {
148-
console.warn(`[${componentName}] - There's a problem with one of your @typedef - ${tag.getText()}`);
180+
if (importData == null) {
181+
console.warn(`[${componentName}] - There's a problem with one of your @import - ${tag.getText()}`);
149182
process.exitCode = 1;
150183
return;
151184
}
152185

153-
// Extract the type from the @typedef import
154-
const typeDefDisplay = tag.name.getText();
186+
const { path: typePath, types: importedTypes } = importData;
155187

156-
const type = types.find((type) => type === typeDefDisplay);
188+
// Check which imported types are actually used in the component
189+
importedTypes.forEach((importedType) => {
190+
const type = types.find((type) => type === importedType);
157191

158-
if (type != null) {
159-
if (!moduleTypeCache.has(typePath)) {
160-
moduleTypeCache.set(typePath, new Set());
192+
if (type != null) {
193+
if (!moduleTypeCache.has(typePath)) {
194+
moduleTypeCache.set(typePath, new Set());
195+
}
196+
moduleTypeCache.get(typePath).add(type);
161197
}
162-
moduleTypeCache.get(typePath).add(type);
163-
}
198+
});
164199
});
165200

166201
// Now that we have the types, and the path of where the types are located
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { LitElement } from 'lit';
2+
3+
/**
4+
* @import {Foo, Bar} from './cc-test-component.types.js'
5+
* @import {TheInterface} from './cc-test-component.types.js'
6+
* @import {TheType} from './cc-test-component.types.js'
7+
*/
8+
9+
/**
10+
* Test component using @import syntax instead of @typedef
11+
*/
12+
// eslint-disable-next-line wc/define-tag-after-class-definition
13+
export class CcTestComponentWithImport extends LitElement {
14+
constructor() {
15+
super();
16+
17+
/** @type {Foo|Bar} - lorem ipsum. */
18+
this.union = null;
19+
20+
/** @type {TheInterface} - lorem ipsum. */
21+
this.interface = null;
22+
23+
/** @type {TheType} - lorem ipsum. */
24+
this.typeDeclaration = null;
25+
}
26+
}

test-mocha/cem/support-typedef-jsdoc-utils.test.js

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'path';
44
import ts from 'typescript';
55
import {
66
convertInterface,
7+
extractImportsFromImportTag,
78
findCustomType,
89
findInterfacesFromExtends,
910
findPathAndTypesFromImports,
@@ -192,26 +193,28 @@ describe('CEM', function () {
192193
describe('findPath()', function () {
193194
const importsNode = classNode.jsDoc[0].tags;
194195
const importLength = importsNode.length;
196+
const moduleFilePath = path.resolve(ROOT_DIR, MODULE_DIR, 'cc-test-component.js');
195197

196198
it('should retrieve the @typedef filePath from the first one in the test file.', function () {
197-
const filePath = findTypePath(importsNode[0], ROOT_DIR, MODULE_DIR);
199+
const filePath = findTypePath(importsNode[0], ts, moduleFilePath);
198200
expect(filePath).to.equal(`${ROOT_DIR}/${MODULE_DIR}/cc-test-component.types.d.ts`);
199201
});
200202

201203
it('should retrieve the common @typedef filePath located at the end of the test file.', function () {
202-
const filePath = findTypePath(importsNode[importLength - 1], ROOT_DIR, MODULE_DIR);
204+
const filePath = findTypePath(importsNode[importLength - 1], ts, moduleFilePath);
203205
expect(filePath).to.equal(`${ROOT_DIR}/${MODULE_DIR}/common.types.d.ts`);
204206
});
205207

206208
it('should return null if the filePath is incorrect.', function () {
207-
const filePath = findTypePath(importsNode[importLength - 2], ROOT_DIR, MODULE_DIR);
209+
const filePath = findTypePath(importsNode[importLength - 2], ts, moduleFilePath);
208210
expect(filePath).to.equal(null);
209211
});
210212
});
211213

212214
describe('findSubtypes()', function () {
213215
const importsNode = classNode.jsDoc[0].tags;
214-
const filePath = findTypePath(importsNode[0], ROOT_DIR, MODULE_DIR);
216+
const moduleFilePath = path.resolve(ROOT_DIR, MODULE_DIR, 'cc-test-component.js');
217+
const filePath = findTypePath(importsNode[0], ts, moduleFilePath);
215218
const sourceCode = readFileSync(filePath).toString();
216219
const sourceAst = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.ES2015, true);
217220

@@ -277,7 +280,8 @@ describe('CEM', function () {
277280
describe('convertInterface()', function () {
278281
it('should return the needed interface in the type file for a given interface name.', function () {
279282
const importsNode = classNode.jsDoc[0].tags;
280-
const filePath = findTypePath(importsNode[0], ROOT_DIR, MODULE_DIR);
283+
const moduleFilePath = path.resolve(ROOT_DIR, MODULE_DIR, 'cc-test-component.js');
284+
const filePath = findTypePath(importsNode[0], ts, moduleFilePath);
281285
const sourceCode = readFileSync(filePath).toString();
282286
const sourceAst = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.ES2015, true);
283287
const interfaceStr = convertInterface(ts, sourceAst, sourceCode, 'TheInterface', filePath);
@@ -316,7 +320,7 @@ describe('CEM', function () {
316320

317321
describe('findPathAndTypesFromImports()', function () {
318322
it('should return an array with all the imports filePath', function () {
319-
const file = 'cc-test-component.types.js';
323+
const file = 'cc-test-component.types.d.ts';
320324
const pathFile = path.resolve(ROOT_DIR, MODULE_DIR, file);
321325
const rootPath = path.resolve(ROOT_DIR, MODULE_DIR);
322326

@@ -332,4 +336,54 @@ describe('CEM', function () {
332336
]);
333337
});
334338
});
339+
340+
describe('extractImportsFromImportTag()', function () {
341+
const filenameWithImport = 'test-mocha/cem/fixtures/cc-test-component-with-import.js';
342+
const sourceCodeWithImport = fs.readFileSync(filenameWithImport, { encoding: 'utf-8' });
343+
const sourceAstWithImport = ts.createSourceFile(
344+
filenameWithImport,
345+
sourceCodeWithImport,
346+
ts.ScriptTarget.ES2015,
347+
true,
348+
);
349+
const classNodeWithImport = sourceAstWithImport.statements.find(
350+
(node) => node.kind === ts.SyntaxKind.ClassDeclaration,
351+
);
352+
const moduleFilePath = path.resolve(ROOT_DIR, MODULE_DIR, 'cc-test-component-with-import.js');
353+
354+
it('should extract module path and types from @import tag', function () {
355+
const importTags =
356+
classNodeWithImport?.jsDoc?.flatMap(
357+
(doc) => doc.tags?.filter((tag) => tag.kind === ts.SyntaxKind.JSDocImportTag) || [],
358+
) || [];
359+
360+
const firstImportTag = importTags[0];
361+
const result = extractImportsFromImportTag(firstImportTag, ts, moduleFilePath);
362+
363+
expect(result).to.not.be.null;
364+
expect(result.path).to.equal(`${ROOT_DIR}/${MODULE_DIR}/cc-test-component.types.d.ts`);
365+
expect(result.types).to.have.members(['Foo', 'Bar']);
366+
});
367+
368+
it('should handle multiple types from same module', function () {
369+
const importTags =
370+
classNodeWithImport?.jsDoc?.flatMap(
371+
(doc) => doc.tags?.filter((tag) => tag.kind === ts.SyntaxKind.JSDocImportTag) || [],
372+
) || [];
373+
374+
const secondImportTag = importTags[1];
375+
const result = extractImportsFromImportTag(secondImportTag, ts, moduleFilePath);
376+
377+
expect(result).to.not.be.null;
378+
expect(result.path).to.equal(`${ROOT_DIR}/${MODULE_DIR}/cc-test-component.types.d.ts`);
379+
expect(result.types).to.have.members(['TheInterface']);
380+
});
381+
382+
it('should return null for invalid import tag', function () {
383+
const invalidTag = { moduleSpecifier: null, importClause: null };
384+
const result = extractImportsFromImportTag(invalidTag, ts, moduleFilePath);
385+
386+
expect(result).to.be.null;
387+
});
388+
});
335389
});

0 commit comments

Comments
 (0)