Skip to content

Commit c6bafea

Browse files
Support explicit and inferred module ids
1 parent ff15459 commit c6bafea

1 file changed

Lines changed: 109 additions & 61 deletions

File tree

index.js

Lines changed: 109 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,13 @@ const fs = require('fs');
44
const env = require('jsdoc/env'); // eslint-disable-line import/no-unresolved
55
const addInherited = require('jsdoc/augment').addInherited; // eslint-disable-line import/no-unresolved
66

7-
const config = env.conf.typescript;
8-
if (!config) {
9-
throw new Error(
10-
'Configuration "typescript" for jsdoc-plugin-typescript missing.'
11-
);
12-
}
13-
if (!('moduleRoot' in config)) {
14-
throw new Error(
15-
'Configuration "typescript.moduleRoot" for jsdoc-plugin-typescript missing.'
16-
);
17-
}
18-
const moduleRoot = config.moduleRoot;
19-
const moduleRootAbsolute = path.join(process.cwd(), moduleRoot);
20-
if (!fs.existsSync(moduleRootAbsolute)) {
7+
const config = env.conf;
8+
const moduleRoot = config.typescript ? config.typescript.moduleRoot : undefined;
9+
const moduleRootAbsolute = moduleRoot
10+
? path.join(process.cwd(), moduleRoot)
11+
: undefined;
12+
13+
if (moduleRootAbsolute && !fs.existsSync(moduleRootAbsolute)) {
2114
throw new Error(
2215
'Directory "' +
2316
moduleRootAbsolute +
@@ -30,31 +23,55 @@ const importRegEx =
3023
const typedefRegEx = /@typedef \{[^\}]*\} (\S+)/g;
3124
const noClassdescRegEx = /@(typedef|module|type)/;
3225
const extensionReplaceRegEx = /\.m?js$/;
26+
const extensionEnsureRegEx = /(\.js)?$/;
3327
const slashRegEx = /\\/g;
3428
const leadingPathSegmentRegEx = /^(.?.[/\\])+/;
3529

3630
const moduleInfos = {};
3731
const fileNodes = {};
3832

39-
function getExtension(absolutePath) {
40-
return extensionReplaceRegEx.test(absolutePath)
41-
? extensionReplaceRegEx.exec(absolutePath)[0]
42-
: '.js';
33+
let inferredModuleRoot;
34+
35+
function getInferredModuleRoot() {
36+
if (inferredModuleRoot) {
37+
return inferredModuleRoot;
38+
}
39+
40+
inferredModuleRoot = env.sourceFiles.reduce((nearestAncestor, curr, i) => {
41+
if (curr.startsWith(nearestAncestor) || i === 0) {
42+
return nearestAncestor;
43+
}
44+
45+
const currParts = curr.split(path.sep);
46+
const nearestParts = nearestAncestor.split(path.sep);
47+
48+
for (let i = 0; i < currParts.length; ++i) {
49+
if (currParts[i] !== nearestParts[i]) {
50+
return currParts.slice(0, i).join(path.sep);
51+
}
52+
}
53+
}, path.dirname(env.sourceFiles[0]));
54+
55+
return inferredModuleRoot;
4356
}
4457

45-
function getModuleInfo(moduleId, extension, parser) {
46-
if (!moduleInfos[moduleId]) {
47-
if (!fileNodes[moduleId]) {
48-
const absolutePath = path.join(moduleRootAbsolute, moduleId + extension);
49-
if (!fs.existsSync(absolutePath)) {
58+
function getModuleInfo(modulePath, parser) {
59+
if (!moduleInfos[modulePath]) {
60+
if (!fileNodes[modulePath]) {
61+
if (!fs.existsSync(modulePath)) {
5062
return null;
5163
}
52-
const file = fs.readFileSync(absolutePath, 'UTF-8');
53-
fileNodes[moduleId] = parser.astBuilder.build(file, absolutePath);
64+
65+
const file = fs.readFileSync(modulePath, 'UTF-8');
66+
67+
fileNodes[modulePath] = parser.astBuilder.build(file, modulePath);
5468
}
55-
moduleInfos[moduleId] = {namedExports: {}};
56-
const moduleInfo = moduleInfos[moduleId];
57-
const node = fileNodes[moduleId];
69+
70+
moduleInfos[modulePath] = {namedExports: {}};
71+
72+
const moduleInfo = moduleInfos[modulePath];
73+
const node = fileNodes[modulePath];
74+
5875
if (node.program && node.program.body) {
5976
const classDeclarations = {};
6077
const nodes = node.program.body;
@@ -77,22 +94,57 @@ function getModuleInfo(moduleId, extension, parser) {
7794
}
7895
}
7996
}
80-
return moduleInfos[moduleId];
97+
98+
return moduleInfos[modulePath];
8199
}
82100

83-
function getDefaultExportName(moduleId, parser) {
84-
return getModuleInfo(moduleId, parser).defaultExport;
101+
function getDefaultExportName(modulePath) {
102+
return getModuleInfo(modulePath).defaultExport;
85103
}
86104

87-
function getDelimiter(moduleId, symbol, parser) {
88-
return getModuleInfo(moduleId, parser).namedExports[symbol] ? '.' : '~';
105+
function getDelimiter(modulePath, symbol) {
106+
return getModuleInfo(modulePath).namedExports[symbol] ? '.' : '~';
89107
}
90108

91109
function getModuleId(modulePath) {
92-
return path
93-
.relative(moduleRootAbsolute, modulePath)
94-
.replace(extensionReplaceRegEx, '')
95-
.replace(leadingPathSegmentRegEx, '');
110+
// Use moduleRoot if set
111+
if (moduleRootAbsolute) {
112+
return path
113+
.relative(moduleRootAbsolute, modulePath)
114+
.replace(extensionReplaceRegEx, '')
115+
.replace(leadingPathSegmentRegEx, '');
116+
}
117+
118+
// Search for explicit module id
119+
if (fileNodes[modulePath]) {
120+
for (const comment of fileNodes[modulePath].comments) {
121+
if (!/@module(?=\s)/.test(comment.value)) {
122+
continue;
123+
}
124+
125+
const explicitModuleId = comment.value
126+
.split(/@module(?=\s)/)[1]
127+
.split(/\n+\s*\*\s*@\w+/)[0] // Split before the next tag
128+
.replace(/\n+\s*\*|\{[^\}]*\}/g, '') // Remove new lines with asterisks, and type annotations
129+
.trim();
130+
131+
if (explicitModuleId) {
132+
return explicitModuleId;
133+
}
134+
}
135+
}
136+
137+
if (getInferredModuleRoot()) {
138+
return path
139+
.relative(inferredModuleRoot, modulePath)
140+
.replace(extensionReplaceRegEx, '');
141+
}
142+
143+
throw new Error(`Unable to resolve module id for file ${modulePath}.`);
144+
}
145+
146+
function withJsExt(filePath) {
147+
return filePath.replace(extensionEnsureRegEx, '.js');
96148
}
97149

98150
exports.defineTags = function (dictionary) {
@@ -146,7 +198,7 @@ exports.defineTags = function (dictionary) {
146198
exports.astNodeVisitor = {
147199
visitNode: function (node, e, parser, currentSourceName) {
148200
if (node.type === 'File') {
149-
fileNodes[getModuleId(currentSourceName)] = node;
201+
fileNodes[currentSourceName] = node;
150202
const identifiers = {};
151203
if (node.program && node.program.body) {
152204
const nodes = node.program.body;
@@ -263,19 +315,18 @@ exports.astNodeVisitor = {
263315
if (identifier) {
264316
const absolutePath = path.resolve(
265317
path.dirname(currentSourceName),
266-
identifier.value
318+
withJsExt(identifier.value)
267319
);
268-
// default to js extension since .js extention is assumed implicitly
269-
const extension = getExtension(absolutePath);
270-
const moduleId = getModuleId(absolutePath);
271320

272-
if (getModuleInfo(moduleId, extension, parser)) {
321+
if (getModuleInfo(absolutePath, parser)) {
322+
const moduleId = getModuleId(absolutePath);
323+
273324
const exportName = identifier.defaultImport
274-
? getDefaultExportName(moduleId, parser)
325+
? getDefaultExportName(absolutePath)
275326
: node.superClass.name;
276327
const delimiter = identifier.defaultImport
277328
? '~'
278-
: getDelimiter(moduleId, exportName, parser);
329+
: getDelimiter(absolutePath, exportName);
279330
lines[lines.length - 2] =
280331
' * @extends ' +
281332
`module:${moduleId.replace(slashRegEx, '/')}${
@@ -332,21 +383,18 @@ exports.astNodeVisitor = {
332383
lastImportPath = importExpression;
333384
const rel = path.resolve(
334385
path.dirname(currentSourceName),
335-
importSource
386+
withJsExt(importSource)
336387
);
337-
// default to js extension since .js extention is assumed implicitly
338-
const extension = getExtension(rel);
339-
const moduleId = getModuleId(rel);
340388

341-
if (getModuleInfo(moduleId, extension, parser)) {
389+
if (getModuleInfo(rel, parser)) {
390+
const moduleId = getModuleId(rel);
391+
342392
const name =
343393
exportName === 'default'
344-
? getDefaultExportName(moduleId, parser)
394+
? getDefaultExportName(rel)
345395
: exportName;
346396
const delimiter =
347-
exportName === 'default'
348-
? '~'
349-
: getDelimiter(moduleId, name, parser);
397+
exportName === 'default' ? '~' : getDelimiter(rel, name);
350398
replacement = `module:${moduleId.replace(slashRegEx, '/')}${
351399
name ? delimiter + name : ''
352400
}`;
@@ -391,18 +439,18 @@ exports.astNodeVisitor = {
391439
const identifier = identifiers[key];
392440
const absolutePath = path.resolve(
393441
path.dirname(currentSourceName),
394-
identifier.value
442+
withJsExt(identifier.value)
395443
);
396-
// default to js extension since .js extention is assumed implicitly
397-
const extension = getExtension(absolutePath);
398-
const moduleId = getModuleId(absolutePath);
399-
if (getModuleInfo(moduleId, extension, parser)) {
444+
445+
if (getModuleInfo(absolutePath, parser)) {
446+
const moduleId = getModuleId(absolutePath);
447+
400448
const exportName = identifier.defaultImport
401-
? getDefaultExportName(moduleId, parser)
449+
? getDefaultExportName(absolutePath)
402450
: key;
403451
const delimiter = identifier.defaultImport
404452
? '~'
405-
: getDelimiter(moduleId, exportName, parser);
453+
: getDelimiter(absolutePath, exportName);
406454
const replacement = `module:${moduleId.replace(
407455
slashRegEx,
408456
'/'

0 commit comments

Comments
 (0)