Skip to content

Commit 8afc3d4

Browse files
committed
first commit
0 parents  commit 8afc3d4

4 files changed

Lines changed: 286 additions & 0 deletions

File tree

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Copyright 2018-present OpenLayers contributors
2+
3+
Redistribution and use in source and binary forms, with or without
4+
modification, are permitted provided that the following conditions are met:
5+
6+
1. Redistributions of source code must retain the above copyright notice, this
7+
list of conditions and the following disclaimer.
8+
9+
2. Redistributions in binary form must reproduce the above copyright notice,
10+
this list of conditions and the following disclaimer in the documentation
11+
and/or other materials provided with the distribution.
12+
13+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# jsdoc-plugin-typescript
2+
3+
Plugin to make TypeScript's JSDoc type annotations work with JSDoc.
4+
5+
## Installation and use
6+
7+
JSDoc accepts plugins by simply installing their npm package:
8+
9+
npm install --save-dev jsdoc-plugin-typescript
10+
11+
To configure JSDoc to use the plugin, add the following to the JSDoc configuration file, e.g. `conf.json`:
12+
13+
```json
14+
"plugins": [
15+
"jsdoc-plugin-typescript"
16+
],
17+
"typescript": {
18+
"moduleRoot": "src"
19+
}
20+
```
21+
22+
See http://usejsdoc.org/about-configuring-jsdoc.html for more details on how to configure JSDoc.
23+
24+
In the above snippet, `"src"` is the directory that contains the source files. Inside that directory, each `.js` file needs a `@module` annotation with a path relative to that `"moduleRoot"`, e.g.
25+
26+
```js
27+
/** @module ol/proj **/
28+
```
29+
30+
## What this plugin does
31+
32+
Types defined in a project are converted to a JSDoc module paths, so they can be documented and linked properly. In addition to types that are used in the same file that they are defined in, imported types are also supported.
33+
34+
TypeScript and JSDoc use a different syntax for imported types:
35+
36+
### TypeScript
37+
38+
**Named export:**
39+
```js
40+
/**
41+
* @type {import("./path/to/module").exportName}
42+
*/
43+
```
44+
45+
**Default export:**
46+
```js
47+
/**
48+
* @type {import("./path/to/module").default}
49+
*/
50+
```
51+
52+
### JSDoc
53+
54+
**Named export:**
55+
```js
56+
/**
57+
* @type {module:path/to/module.exportName}
58+
*/
59+
```
60+
61+
**Default export assigned to a variable in the exporting module:**
62+
```js
63+
/**
64+
* @type {module:path/to/module~variableOfDefaultExport}
65+
*/
66+
```
67+
68+
This syntax is also used when refering to types of `@typedef`s and `@enum`s.
69+
70+
**Anonymous default export:**
71+
```js
72+
/**
73+
* @type {module:path/to/module}
74+
*/
75+
```

index.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
const env = require('jsdoc/env');
4+
5+
const config = env.conf.typescript;
6+
if (!config) {
7+
throw new Error('Configuration "typescript" for jsdoc-plugin-typescript missing.');
8+
}
9+
if (!'moduleRoot' in config) {
10+
throw new Error('Configuration "typescript.moduleRoot" for jsdoc-plugin-typescript missing.');
11+
}
12+
const moduleRoot = config.moduleRoot;
13+
const moduleRootAbsolute = path.join(process.cwd(), moduleRoot);
14+
if (!fs.existsSync(moduleRootAbsolute)) {
15+
throw new Error('Directory "' + moduleRootAbsolute + '" does not exist. Check the "typescript.moduleRoot" config option for jsdoc-plugin-typescript');
16+
}
17+
18+
const importRegEx = /(typeof )?import\("([^"]*)"\)\.([^ \.\|\}><,\)=\n]*)([ \.\|\}><,\)=\n])/g;
19+
const typedefRegEx = /@typedef \{[^\}]*\} ([^ \r?\n?]*)/;
20+
21+
const moduleInfos = {};
22+
const fileNodes = {};
23+
24+
function getModuleInfo(moduleId, parser) {
25+
if (!moduleInfos[moduleId]) {
26+
const moduleInfo = moduleInfos[moduleId] = {
27+
namedExports: {}
28+
};
29+
if (!fileNodes[moduleId]) {
30+
const classDeclarations = {};
31+
const absolutePath = path.join(process.cwd(), moduleRoot, moduleId + '.js');
32+
const file = fs.readFileSync(absolutePath, 'UTF-8');
33+
const node = fileNodes[moduleId] = parser.astBuilder.build(file, absolutePath);
34+
if (node.program && node.program.body) {
35+
const nodes = node.program.body;
36+
for (let i = 0, ii = nodes.length; i < ii; ++i) {
37+
const node = nodes[i];
38+
if (node.type === 'ClassDeclaration') {
39+
classDeclarations[node.id.name] = node;
40+
} else if (node.type === 'ExportDefaultDeclaration') {
41+
const classDeclaration = classDeclarations[node.declaration.name];
42+
if (classDeclaration) {
43+
moduleInfo.defaultExport = classDeclaration.id.name;
44+
}
45+
} else if (node.type === 'ExportNamedDeclaration' && node.declaration && node.declaration.type === 'ClassDeclaration') {
46+
moduleInfo.namedExports[node.declaration.id.name] = true;
47+
}
48+
}
49+
}
50+
}
51+
}
52+
return moduleInfos[moduleId];
53+
}
54+
55+
function getDefaultExportName(moduleId, parser) {
56+
return getModuleInfo(moduleId, parser).defaultExport;
57+
}
58+
59+
function getDelimiter(moduleId, symbol, parser) {
60+
return getModuleInfo(moduleId, parser).namedExports[symbol] ? '.' : '~'
61+
}
62+
63+
exports.astNodeVisitor = {
64+
65+
visitNode: function(node, e, parser, currentSourceName) {
66+
if (node.type === 'File') {
67+
const relPath = path.relative(process.cwd(), currentSourceName);
68+
const modulePath = path.relative(path.join(process.cwd(), moduleRoot), currentSourceName).replace(/\.js$/, '');
69+
fileNodes[modulePath] = node;
70+
const identifiers = {};
71+
if (node.program && node.program.body) {
72+
const nodes = node.program.body;
73+
for (let i = 0, ii = nodes.length; i < ii; ++i) {
74+
let node = nodes[i];
75+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
76+
node = node.declaration;
77+
}
78+
if (node.type === 'ImportDeclaration') {
79+
node.specifiers.forEach(specifier => {
80+
let defaultImport = false;
81+
switch (specifier.type) {
82+
case 'ImportDefaultSpecifier':
83+
defaultImport = true;
84+
// fallthrough
85+
case 'ImportSpecifier':
86+
identifiers[specifier.local.name] = {
87+
defaultImport,
88+
value: node.source.value
89+
};
90+
break;
91+
default:
92+
}
93+
});
94+
} else if (node.type === 'ClassDeclaration') {
95+
if (node.id && node.id.name) {
96+
identifiers[node.id.name] = {
97+
value: path.basename(currentSourceName)
98+
};
99+
}
100+
101+
// Add class inheritance information because JSDoc does not honor
102+
// the ES6 class's `extends` keyword
103+
if (node.superClass && node.leadingComments) {
104+
const leadingComment = node.leadingComments[node.leadingComments.length - 1];
105+
const lines = leadingComment.value.split(/\r?\n/);
106+
lines.push(lines[lines.length - 1]);
107+
const identifier = identifiers[node.superClass.name];
108+
if (identifier) {
109+
const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value);
110+
const moduleId = path.relative(path.join(process.cwd(), moduleRoot), absolutePath).replace(/\.js$/, '');
111+
const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : node.superClass.name;
112+
const delimiter = identifier.defaultImport ? '~' : getDelimiter(moduleId, exportName, parser);
113+
lines[lines.length - 2] = ' * @extends ' + `module:${moduleId}${exportName ? delimiter + exportName : ''}`;
114+
} else {
115+
lines[lines.length - 2] = ' * @extends ' + node.superClass.name;
116+
}
117+
leadingComment.value = lines.join('\n');
118+
}
119+
120+
}
121+
}
122+
}
123+
if (node.comments) {
124+
node.comments.forEach(comment => {
125+
//TODO Handle typeof, to indicate that a constructor instead of an
126+
// instance is needed.
127+
comment.value = comment.value.replace(/typeof /g, '');
128+
129+
// Convert `import("path/to/module").export` to
130+
// `module:path/to/module~Name`
131+
let importMatch;
132+
while ((importMatch = importRegEx.exec(comment.value))) {
133+
importRegEx.lastIndex = 0;
134+
let replacement;
135+
if (importMatch[2].charAt(0) !== '.') {
136+
// simplified replacement for external packages
137+
replacement = `module:${importMatch[2]}${importMatch[3] === 'default' ? '' : '~' + importMatch[3]}`;
138+
} else {
139+
const rel = path.resolve(path.dirname(currentSourceName), importMatch[2]);
140+
const importModule = path.relative(path.join(process.cwd(), moduleRoot), rel).replace(/\.js$/, '');
141+
const exportName = importMatch[3] === 'default' ? getDefaultExportName(importModule, parser) : importMatch[3];
142+
const delimiter = importMatch[3] === 'default' ? '~': getDelimiter(importModule, exportName, parser);
143+
replacement = `module:${importModule}${exportName ? delimiter + exportName : ''}`;
144+
}
145+
comment.value = comment.value.replace(importMatch[0], replacement + importMatch[4]);
146+
}
147+
148+
// Treat `@typedef`s like named exports
149+
const typedefMatch = comment.value.replace(/\r?\n?\s*\*\s/g, ' ').match(typedefRegEx);
150+
if (typedefMatch) {
151+
identifiers[typedefMatch[1]] = {
152+
value: path.basename(currentSourceName)
153+
};
154+
}
155+
156+
// Replace local types with the full `module:` path
157+
Object.keys(identifiers).forEach(key => {
158+
const regex = new RegExp(`(@fires |[\{<\|,] ?)${key}`, 'g');
159+
if (regex.test(comment.value)) {
160+
const identifier = identifiers[key];
161+
const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value);
162+
const moduleId = path.relative(path.join(process.cwd(), moduleRoot), absolutePath).replace(/\.js$/, '');
163+
const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : key;
164+
const delimiter = identifier.defaultImport ? '~' : getDelimiter(moduleId, exportName, parser);
165+
const replacement = `module:${moduleId}${exportName ? delimiter + exportName : ''}`;
166+
comment.value = comment.value.replace(regex, '$1' + replacement);
167+
}
168+
});
169+
});
170+
}
171+
}
172+
}
173+
174+
};

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "jsdoc-plugin-typescript",
3+
"version": "1.0.0",
4+
"description": "Plugin to make TypeScript's JSDoc type annotations work with JSDoc",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"keywords": ["jsdoc", "typescript"],
10+
"license": "BSD-2-Clause",
11+
"repository": {
12+
"type": "git",
13+
"url": "git://github.com/openlayers/jsdoc-plugin-typescript.git"
14+
}
15+
}

0 commit comments

Comments
 (0)