feat!: replace dgeni/gulp with TypeDoc for README generation

Replace the legacy dgeni/gulp documentation pipeline with TypeDoc
and typedoc-plugin-markdown. Generates plugin README files with
extracted @Plugin() decorator metadata. Register custom JSDoc tags
used by plugin source files.
This commit is contained in:
Daniel Sogl
2026-03-21 15:10:30 -07:00
parent a5e3ccd185
commit 76b4e03d04
283 changed files with 1249 additions and 1704 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"v2DocsDir": "docs/native",
"pluginDir": "dist/@awesome-cordova-plugins/plugins"
}
-3
View File
@@ -1,3 +0,0 @@
module.exports = function (getLinkInfo) {
getLinkInfo.useFirstAmbiguousLink = false;
};
-3
View File
@@ -1,3 +0,0 @@
module.exports = function (log) {
log.level = 'error'; //'silly', 'debug', 'info', 'warn', 'error'
};
-3
View File
@@ -1,3 +0,0 @@
module.exports = function (parseTagsProcessor) {
parseTagsProcessor.tagDefinitions = parseTagsProcessor.tagDefinitions.concat(require('../tag-defs/tag-defs'));
};
@@ -1,12 +0,0 @@
module.exports = function (templateEngine) {
// Nunjucks and Angular conflict in their template bindings so change the Nunjucks
// Also conflict with Jekyll
templateEngine.config.tags = {
variableStart: '<$',
variableEnd: '$>',
blockStart: '<@',
blockEnd: '@>',
commentStart: '<#',
commentEnd: '#>',
};
};
@@ -1,9 +0,0 @@
module.exports = function (templateEngine) {
// add custom filters to nunjucks
templateEngine.filters.push(
require('../filters/capital'),
require('../filters/code'),
require('../filters/dump'),
require('../filters/dashify')
);
};
-75
View File
@@ -1,75 +0,0 @@
'use strict';
const Package = require('dgeni').Package,
jsdocPackage = require('dgeni-packages/jsdoc'),
nunjucksPackage = require('dgeni-packages/nunjucks'),
typescriptPackage = require('dgeni-packages/typescript'),
linksPackage = require('dgeni-packages/links'),
path = require('path'),
config = require('../config.json');
module.exports = (currentVersion) => {
return (
new Package('ionic-native-docs', [jsdocPackage, nunjucksPackage, typescriptPackage, linksPackage])
.processor(require('./processors/remove-private-members'))
.processor(require('./processors/hide-private-api'))
.processor(require('./processors/parse-optional'))
.processor(require('./processors/mark-properties'))
.processor(require('./processors/npm-id'))
.processor(require('./processors/jekyll'))
.config(require('./configs/log'))
.config(require('./configs/template-filters'))
.config(require('./configs/template-tags'))
.config(require('./configs/tag-defs'))
.config(require('./configs/links'))
.config(function (renderDocsProcessor, computePathsProcessor) {
currentVersion = {
href: '/' + config.v2DocsDir.replace('content/', ''),
folder: '',
name: currentVersion,
};
renderDocsProcessor.extraData.version = {
list: [currentVersion],
current: currentVersion,
latest: currentVersion,
};
computePathsProcessor.pathTemplates = [
{
docTypes: ['class'],
getOutputPath: (doc) => 'content/' + config.v2DocsDir + '/' + doc.name + '/index.md',
},
];
})
//configure file reading
.config(function (readFilesProcessor, readTypeScriptModules) {
// Don't run unwanted processors since we are not using the normal file reading processor
readFilesProcessor.$enabled = false;
readFilesProcessor.basePath = path.resolve(__dirname, '../../..');
readTypeScriptModules.basePath = path.resolve(__dirname, '../../..');
readTypeScriptModules.sourceFiles = ['./src/@awesome-cordova-plugins/plugins/**/*.ts'];
})
// Configure file writing
.config(function (writeFilesProcessor) {
writeFilesProcessor.outputFolder = '../ionic-site/';
})
// Configure rendering
.config(function (templateFinder) {
templateFinder.templateFolders.unshift(path.resolve(__dirname, 'templates'));
// Specify how to match docs to templates.
templateFinder.templatePatterns = [
'${ doc.template }',
'${ doc.docType }.template.html',
'common.template.html',
];
})
);
};
@@ -1,74 +0,0 @@
'use strict';
const Package = require('dgeni').Package,
jsdocPackage = require('dgeni-packages/jsdoc'),
nunjucksPackage = require('dgeni-packages/nunjucks'),
typescriptPackage = require('dgeni-packages/typescript'),
linksPackage = require('dgeni-packages/links'),
path = require('path'),
config = require('../config.json');
module.exports = (currentVersion) => {
return (
new Package('ionic-native-readmes', [jsdocPackage, nunjucksPackage, typescriptPackage, linksPackage])
.processor(require('./processors/readmes'))
.processor(require('./processors/remove-private-members'))
.processor(require('./processors/hide-private-api'))
.processor(require('./processors/npm-id'))
.config(require('./configs/log'))
.config(require('./configs/template-filters'))
.config(require('./configs/template-tags'))
.config(require('./configs/tag-defs'))
.config(require('./configs/links'))
.config(function (renderDocsProcessor, computePathsProcessor) {
currentVersion = {
href: '/' + config.v2DocsDir.replace('content/', ''),
folder: '',
name: currentVersion,
};
renderDocsProcessor.extraData.version = {
list: [currentVersion],
current: currentVersion,
latest: currentVersion,
};
computePathsProcessor.pathTemplates = [
{
docTypes: ['class'],
getOutputPath: (doc) =>
doc.originalModule.replace(config.pluginDir + '/', '').replace(/\/index$/, '/README.md'),
},
];
})
//configure file reading
.config(function (readFilesProcessor, readTypeScriptModules) {
// Don't run unwanted processors since we are not using the normal file reading processor
readFilesProcessor.$enabled = false;
readFilesProcessor.basePath = path.resolve(__dirname, '../../..');
readTypeScriptModules.basePath = path.resolve(path.resolve(__dirname, '../../..'));
readTypeScriptModules.sourceFiles = ['./src/@awesome-cordova-plugins/plugins/**/*.ts'];
})
// Configure file writing
.config(function (writeFilesProcessor) {
writeFilesProcessor.outputFolder = './dist/@awesome-cordova-plugins/';
})
.config(function (writeFilesProcessor) {
writeFilesProcessor.outputFolder = './docs/';
})
// Configure rendering
.config(function (templateFinder) {
templateFinder.templateFolders.unshift(path.resolve(__dirname, 'templates'));
// Specify how to match docs to templates.
templateFinder.templatePatterns = ['${ doc.template }', '${ doc.docType }.template.md', 'readme.template.md'];
})
);
};
-5
View File
@@ -1,5 +0,0 @@
'use strict';
module.exports = {
name: 'capital',
process: (str) => (str ? str.charAt(0).toUpperCase() + str.substring(1) : ''),
};
-24
View File
@@ -1,24 +0,0 @@
'use strict';
const encoder = new require('node-html-encoder').Encoder();
function code(str, inline, lang) {
// Encode any HTML entities in the code string
str = encoder.htmlEncode(str, true);
// If a language is provided then attach a CSS class to the code element
lang = lang ? ' class="lang-' + lang + '"' : '';
str = '<code' + lang + '>' + str + '</code>';
// If not inline then wrap the code element in a pre element
if (!inline) {
str = '<pre>' + str + '</pre>';
}
return str;
}
module.exports = {
name: 'code',
process: (str, lang) => code(str, true, lang),
};
-5
View File
@@ -1,5 +0,0 @@
'use strict';
module.exports = {
name: 'dashify',
process: (str) => (str ? str.replace(/\s/g, '-') : ''),
};
-5
View File
@@ -1,5 +0,0 @@
'use strict';
module.exports = {
name: 'dump',
process: (obj) => console.log(obj),
};
-39
View File
@@ -1,39 +0,0 @@
'use strict';
module.exports = function test() {
return {
name: 'debug',
$runBefore: ['rendering-docs'],
$process: function (docs) {
docs.forEach(function (doc) {
if (doc.name == 'Camera') {
console.log(doc.tags);
doc.tags.forEach(function (tag) {
if (tag.tagName == 'classes') {
}
});
doc.moduleDoc.exports.forEach(function (d, i) {
if (d.name === 'CameraOptions') {
console.log('Name: ' + d.name);
console.log('Type: ' + d.docType);
console.log('First member: ', d.members[0]);
}
});
var exports = doc.exportSymbol.parent.exports;
for (var p in exports) {
if (p == 'CameraOptions') {
var x = exports[p];
console.log(x.members.quality);
}
}
doc.members.forEach(function (method) {
if (method.name === 'getPicture') {
console.log(method);
}
});
}
});
},
};
};
@@ -1,9 +0,0 @@
'use strict';
module.exports = function removePrivateApi() {
return {
name: 'remove-private-api',
description: 'Prevent the private apis from being rendered',
$runBefore: ['rendering-docs'],
$process: (docs) => docs.filter((doc) => !doc.private && (!doc.tags || !doc.tags.tagsByName.get('hidden'))),
};
};
-63
View File
@@ -1,63 +0,0 @@
'use strict';
module.exports = function jekyll(renderDocsProcessor) {
return {
name: 'jekyll',
description: 'Create jekyll includes',
$runAfter: ['paths-computed'],
$runBefore: ['rendering-docs'],
$process: (docs) => {
// pretty up and sort the docs object for menu generation
docs = docs.filter((doc) => (!!doc.name && !!doc.outputPath) || doc.docType === 'index-page');
docs.push({
docType: 'class',
URL: 'https://github.com/ionic-team/ionic-native-google-maps/blob/master/documents/README.md',
name: 'Google Maps',
});
docs.sort((a, b) => {
const textA = a.name ? a.name.toUpperCase() : '',
textB = b.name ? b.name.toUpperCase() : '';
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
docs.forEach((doc) => {
if (!doc.outputPath) {
return;
}
doc.outputPath = doc.outputPath.toLowerCase().replace(/\s/g, '-');
doc.URL = doc.outputPath.replace('docs//', 'docs/').replace('/index.md', '').replace('content/', '');
// add trailing slash to plugin pages
if (!doc.URL.endsWith('/') && !doc.URL.endsWith('.html')) {
doc.URL = doc.URL + '/';
}
doc.URL = '/' + doc.URL;
});
const betaDocs = [];
docs = docs.filter((doc) => {
if (doc.beta === true) {
betaDocs.push(doc);
return false;
}
return true;
});
docs = docs.concat(betaDocs);
// add side menu
docs.push({
docType: 'nativeMenu',
id: 'native_menu',
template: 'native_menu.template.html',
outputPath: 'content/_includes/fluid/native_menu.html',
});
return docs;
},
};
};
@@ -1,16 +0,0 @@
'use strict';
module.exports = function markProperties() {
return {
name: 'mark-properties',
$runBefore: ['rendering-docs'],
$process: (docs) =>
docs.map((doc) => {
for (let i in doc.members) {
if (doc.members.hasOwnProperty(i) && typeof doc.members[i].parameters === 'undefined') {
doc.members[i].isProperty = true;
}
}
return doc;
}),
};
};
-20
View File
@@ -1,20 +0,0 @@
'use strict';
module.exports = function npmId(renderDocsProcessor) {
return {
name: 'npm-id',
$runAfter: ['paths-computed'],
$runBefore: ['rendering-docs'],
$process: (docs) => {
// pretty up and sort the docs object for menu generation
docs = docs.filter(function (doc) {
return (!!doc.name && !!doc.outputPath) || doc.docType === 'index-page';
});
docs.forEach((doc) => {
doc.npmId = doc.id.match(/plugins\/(.*)\/index/)[1];
});
return docs;
},
};
};
@@ -1,22 +0,0 @@
'use strict';
module.exports = function parseOptional() {
return {
$runBefore: ['rendering-docs'],
$process: (docs) => {
docs.forEach((doc) => {
if (doc.members && doc.members.length) {
for (let i in doc.members) {
if (doc.members[i].params && doc.members[i].params.length) {
for (let ii in doc.members[i].params) {
if (doc.members[i].params[ii].optional) {
doc.members[i].params[ii].description += '<strong class="tag">Optional</strong>';
}
}
}
}
}
});
return docs;
},
};
};
-19
View File
@@ -1,19 +0,0 @@
'use strict';
module.exports = function readmes(renderDocsProcessor) {
return {
name: 'readmes',
description: 'Create jekyll includes',
$runAfter: ['paths-computed'],
$runBefore: ['rendering-docs'],
$process: (docs) => {
// pretty up and sort the docs object for menu generation
docs = docs.filter((doc) => (!!doc.name && !!doc.outputPath) || doc.docType === 'index-page');
docs.forEach((doc) => {
doc.outputPath = doc.outputPath.replace('src/@awesome-cordova-plugins/', '');
});
return docs;
},
};
};
@@ -1,22 +0,0 @@
'use strict';
module.exports = function removePrivateMembers() {
return {
name: 'remove-private-members',
description: 'Remove member docs with @private tags',
$runAfter: ['tags-parsed'],
$runBefore: ['rendering-docs'],
$process: (docs) => {
docs.forEach((doc) => {
if (doc.members) {
doc.members = doc.members.filter((member) => !member.tags.tagsByName.get('hidden'));
}
if (doc.statics) {
doc.statics = doc.statics.filter((staticMethod) => !staticMethod.tags.tagsByName.get('hidden'));
}
});
return docs;
},
};
};
-11
View File
@@ -1,11 +0,0 @@
'use strict';
module.exports = [
{ name: 'advanced' },
{ name: 'demo' },
{ name: 'beta', transforms: (doc, tag, value) => typeof value !== 'undefined' }, // make the value true or undefined instead of '' or undefined
{ name: 'usage' },
{ name: 'hidden' }, // hide from docs
{ name: 'classes' }, // related classes
{ name: 'interfaces' }, // related interfaces
{ name: 'paid', transforms: (doc, tag, value) => typeof value !== 'undefined' }, // paid plugin, set value to true
];
@@ -1,192 +0,0 @@
---
layout: "fluid/docs_base"
version: "<$ version.current.name $>"
versionHref: "<$ version.current.href.replace('content/','') $>"
path: "<$ doc.path $>"
category: native
id: "<$ doc.name|lower|replace(' ','-') $>"
title: "<@ if doc.docType == "directive" @><$ doc.name | dashCase $><@ else @><$ doc.name $><@ endif @>"
header_sub_title: "<$ doc.docType | capital $> in module <$ doc.module $>"
doc: "<$ doc.name $>"
docType: "<$ doc.docType $>"
---
<@- macro interfaceTable(interface) -@> <@ for export in doc.moduleDoc.exports -@> <@ if export.name == interface @>
<table class="table param-table" style="margin: 0">
<thead>
<tr>
<th>Param</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<@ for param in export.members @>
<tr>
<td><$ param.name $></td>
<td>
<code><$ param.returnType | escape $></code>
</td>
<td><$ param.description | marked $> <@ if param.optional @><em>(optional)</em><@ endif @></td>
</tr>
<@ endfor @>
</tbody>
</table>
<@ endif @> <@- endfor @> <@- endmacro -@> <@- macro githubViewLink(doc) -@>
<a
href="https://github.com/ionic-team/ionic-native/tree/master/<$ doc.fileInfo.relativePath $>#L<$ doc.location.start.line+1 $>-L<$ doc.location.end.line+1 $>"
><$ doc.fileInfo.relativePath $> (line <$ doc.location.start.line+1 $>)</a
>
<@- endmacro -@> <@- macro paramTable(params, isDirective) -@>
<table class="table param-table" style="margin: 0">
<thead>
<tr>
<th><@ if isDirective @>Attr<@ else @>Param<@ endif @></th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<@- for param in params @>
<tr>
<td><$ param.name $> <@- if param.alias @>| <$ param.alias $><@ endif -@></td>
<td><$ typeList(param.typeList) $></td>
<td>
<$ param.description | marked $> <@- if param.defaultValue @>
<p><em>(default: <$ param.defaultValue $>)</em></p>
<@ endif -@>
</td>
</tr>
<@ endfor -@>
</tbody>
</table>
<@- endmacro -@> <@- macro functionSyntax(fn) @> <@- set sep = joiner(',&nbsp;') -@>
<code
><$ fn.name $><@- if not fn.isProperty @>(<@ endif -@><@- for param in fn.params @><$ sep() $> <@- if
param.type.optional @>[<@- endif -@> <$ param.name $> <@- if param.type.optional -@>]<@- endif -@> <@- endfor -@><@-
if not fn.isProperty @>)<@- endif -@></code
>
<@- endmacro -@> <@- macro typeList(types) -@> <@ set separator = joiner("|") @> <@- for type in types @><$ separator()
$><$ type | code $><@ endfor -@> <@- endmacro -@> <@- macro typeInfo(fn) -@> <$ typeList(fn.typeList) $> <$
fn.description $> <@- endmacro -@> <@- macro documentPlatforms(method) -@> <@- if method.decorators @> <@ for prop in
method.decorators[0].argumentInfo @> <@ if prop.platforms @>
<p>
<strong>Platforms:</strong>
<@- for platform in prop.platforms -@>
<strong class="tag"><$ platform $></strong>&nbsp; <@- endfor -@>
</p>
<@ endif @> <@ endfor @> <@- endif @> <@- endmacro -@> <@ macro documentMethod(method) -@>
<h3><a class="anchor" name="<$ method.name $>" href="#<$ method.name $>"></a><$ functionSyntax(method) $></h3>
<$ documentPlatforms(method) $> <$ method.description $> <@ if method.params -@> <$ paramTable(method.params) $> <@-
endif @> <@ if method.returns -@>
<div class="return-value" markdown="1">
<i class="icon ion-arrow-return-left"></i>
<b>Returns:</b> <$ typeInfo(method.returns) $>
</div>
<@- endif @> <@- endmacro -@> <@- macro documentClass(doc) @> <@- if doc.statics.length -@>
<h2><a class="anchor" name="static-members" href="#static-members"></a>Static Members</h2>
<@ for method in doc.statics -@> <$ documentMethod(method) $> <@ endfor -@> <@ endif @> <# --- methods in class --- #>
<@- if doc.members and doc.members.length @>
<h2><a class="anchor" name="instance-members" href="#instance-members"></a>Instance Members</h2>
<@ for method in doc.members -@> <$ documentMethod(method) $> <@- endfor @> <@- endif -@> <@ endmacro @>
<h1 class="api-title">
<$ doc.name $> <@- if doc.beta == true -@>
<span class="beta" title="beta">&beta;</span>
<@- endif -@> <@- if doc.paid == true -@>
<span class="paid" title="paid">Paid</span>
<@- endif -@>
</h1>
<a
class="improve-v2-docs"
href="http://github.com/ionic-team/ionic-native/edit/master/<$ doc.fileInfo.relativePath|replace('/home/ubuntu/ionic-native/', '')|replace('//','/') $>#L<$ doc.location.start.line $>"
>
Improve this doc
</a>
<# --- Decorators --- #> <@- if doc.decorators @> <@ for prop in doc.decorators[0].argumentInfo @> <@ if doc.beta ==
true @>
<p class="beta-notice">
This plugin is still in beta stage and may not work as expected. Please submit any issues to the
<a target="_blank" href="<$ prop.repo $>/issues">plugin repo</a>.
</p>
<@ endif @> <@ if doc.paid == true @>
<p class="paid-notice">
This plugin might require a paid license, or might take a share of your app's earnings. Check the
<a target="_blank" rel="nofollow" href="<$ prop.repo $>">plugin's repo</a> for more information.
</p>
<@ endif @> <# --- Plugin description --- #> <$ doc.description | marked $>
<p>
Repo:
<a href="<$ prop.repo $>"> <$ prop.repo $> </a>
</p>
<# --- Install commands --- #>
<h2><a class="anchor" name="installation" href="#installation"></a>Installation</h2>
<ol class="installation">
<li>
Install the Cordova and Ionic Native plugins:<br />
<pre><code class="nohighlight">$ <@ if prop.install @><$ prop.install | replace('<', '&lt;').replace('>', '&gt;') $><@ else @>ionic cordova plugin add <$ prop.plugin $><@ endif @>
$ npm install @ionic-native/<$ doc.npmId $>
</code></pre>
</li>
<li>
<a href="https://ionicframework.com/docs/native/#Add_Plugins_to_Your_App_Module"
>Add this plugin to your app's module</a
>
</li>
</ol>
<# --- Plugin supported platforms --- #> <@ if prop.platforms @>
<h2><a class="anchor" name="platforms" href="#platforms"></a>Supported platforms</h2>
<ul>
<@ for platform in prop.platforms -@>
<li><$ platform $></li>
<@- endfor @>
</ul>
<@ endif @> <@ endfor @> <@ endif -@> <# --- end of: if doc.decorators --- #> <# --- Plugin usage --- #> <@ if doc.usage
@>
<h2><a class="anchor" name="usage" href="#usage"></a>Usage</h2>
<$ doc.usage | marked $> <@ endif @> <# --- Plugin attributes --- #> <@- if doc.properties -@>
<h2><a class="anchor" name="attributes" href="#attributes"></a>Attributes:</h2>
<table class="table" style="margin: 0">
<thead>
<tr>
<th>Attribute</th>
<@ set hasTypes = false @> <@ for prop in doc.properties @> <@ if prop.type @> <@ set hasTypes = true @> <@ endif
@> <@ endfor @> <@ if hasTypes @>
<th>Type</th>
<@ endif @>
<th>Description</th>
</tr>
</thead>
<tbody>
<@- for prop in doc.properties -@>
<tr>
<td><$ prop.name $></td>
<@ if hasTypes @>
<td><$ prop.type.name $></td>
<@ endif @>
<td><$ prop.description $></td>
</tr>
<@ endfor -@>
</tbody>
</table>
<@- endif -@> <# --- Plugin class documentation --- #> <$ documentClass(doc) $> <# --- Advanced usage --- #> <@- if
doc.advanced -@>
<h2><a class="anchor" name="advanced" href="#advanced"></a>Advanced</h2>
<$ doc.advanced | marked $> <@- endif -@> <# --- Other classes --- #> <@- for tag in doc.tags.tags -@> <@- if
tag.tagName == 'classes' -@> <@- set classes = tag.description.split('\n') -@> <@- for item in classes -@> <@- if
item.length > 1 -@> <@- for export in doc.moduleDoc.exports -@> <@- if export.name == item -@>
<h2><a class="anchor" name="<$ item $>" href="#<$ item $>"></a><$ item $></h2>
<$ documentClass(export) $> <@- endif -@> <@- endfor -@> <@- endif -@> <@- endfor -@> <@- endif -@> <@- endfor -@> <#
--- Other interfaces --- #> <@ for tag in doc.tags.tags -@> <@ if tag.tagName == 'interfaces' @> <@ set interfaces =
tag.description.split('\n') @> <@ for item in interfaces -@> <@ if item.length > 1 @>
<h2><a class="anchor" name="<$ item $>" href="#<$ item $>"></a><$ item $></h2>
<$ interfaceTable(item) $> <@ endif @> <@- endfor @> <@ endif @> <@- endfor @> <# --- Related links --- #> <@- if
doc.see @>
<h2><a class="anchor" name="related" href="#related"></a>Related</h2>
<@ for s in doc.see @> <$ s | safe $> <@- endfor -@> <@- endif -@>
@@ -1,14 +0,0 @@
<li class="capitalize {% if page.id == 'overview' %}active{% endif %}">
<a href="/docs/native/">Overview</a>
</li>
<li class="capitalize {% if page.id == 'mocking' %}active{% endif %}">
<a href="/docs/native/browser.html">Browser Usage</a>
</li>
<@- for doc in docs @><@ if doc.URL and doc.private != true @>
<li class="capitalize {% if page.id == '<$ doc.name|lower|dashify $>' %}active{% endif %}">
<a href="<$ doc.URL $>"
><$ doc.name $><@ if doc.paid == true @> <span class="paid">Paid</span><@ endif @><@ if doc.beta == true @>
<span class="beta">&beta;</span><@ endif @></a
>
</li>
<@ endif @><@ endfor @>
@@ -1,36 +0,0 @@
# <$ doc.name $>
<@- if doc.beta == true @>
<p style="color:orange">
This plugin is still in beta stage and may not work as expected. Please
submit any issues to the <a target="_blank"
href="<$ prop.repo $>/issues">plugin repo</a>.
</p>
<@ endif -@>
<@ for prop in doc.decorators[0].argumentInfo @>
```
$ <@ if prop.install @><$ prop.install $><@ else @>ionic cordova plugin add <$ prop.plugin $><@ endif @>
$ npm install @awesome-cordova-plugins/<$ doc.npmId|replace('plugins/','') $>
```
## [Usage Documentation](https://danielsogl.gitbook.io/awesome-cordova-plugins/plugins/<$ doc.fileInfo.relativePath|replace('/home/ubuntu/ionic-native/', '')|replace('//','/')|replace('index.ts','')|replace('src/@awesome-cordova-plugins/plugins/','') $>)
Plugin Repo: [<$ prop.repo $>](<$ prop.repo $>)
<$ doc.description $>
<@- if prop.platforms @>
## Supported platforms
<@ for platform in prop.platforms -@>
- <$ platform $>
<@ endfor @>
<@ endif -@>
<@ endfor @>
+257
View File
@@ -0,0 +1,257 @@
import {
Application,
Converter,
Context,
ReflectionKind,
DeclarationReflection,
CommentTag,
ProjectReflection,
LogLevel,
} from 'typedoc';
import { cpSync, mkdirSync, writeFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import {
Node,
Symbol as TsSymbol,
isClassDeclaration,
isCallExpression,
isIdentifier,
isObjectLiteralExpression,
isPropertyAssignment,
isStringLiteral,
isNumericLiteral,
isArrayLiteralExpression,
getDecorators,
SyntaxKind,
} from 'typescript';
const ROOT = resolve(__dirname, '../..');
const PLUGINS_SRC = join(ROOT, 'src/@awesome-cordova-plugins/plugins');
const DOCS_OUT = join(ROOT, 'docs/plugins');
interface PluginMeta {
pluginName?: string;
plugin?: string;
pluginRef?: string;
repo?: string;
platforms?: string[];
install?: string;
}
// Map from reflection id to extracted decorator metadata
const pluginMetaMap = new Map<number, PluginMeta>();
function parseLiteralValue(node: Node): string | number | boolean | string[] | undefined {
if (isStringLiteral(node)) return node.text;
if (isNumericLiteral(node)) return Number(node.text);
if (node.kind === SyntaxKind.TrueKeyword) return true;
if (node.kind === SyntaxKind.FalseKeyword) return false;
if (isArrayLiteralExpression(node)) {
return node.elements.map((el) => {
const val = parseLiteralValue(el);
return typeof val === 'string' ? val : String(val);
});
}
return undefined;
}
function extractPluginMeta(symbol: TsSymbol | undefined): PluginMeta | undefined {
if (!symbol) return undefined;
const decl = symbol.declarations?.[0];
if (!decl || !isClassDeclaration(decl)) return undefined;
const decorators = getDecorators(decl);
if (!decorators) return undefined;
const pluginDec = decorators.find((d) => {
const expr = d.expression;
return isCallExpression(expr) && isIdentifier(expr.expression) && expr.expression.text === 'Plugin';
});
if (!pluginDec) return undefined;
const callExpr = pluginDec.expression;
if (!isCallExpression(callExpr)) return undefined;
const args = callExpr.arguments[0];
if (!args || !isObjectLiteralExpression(args)) return undefined;
const meta: Record<string, string | number | boolean | string[] | undefined> = {};
for (const prop of args.properties) {
if (!isPropertyAssignment(prop) || !isIdentifier(prop.name)) continue;
meta[prop.name.text] = parseLiteralValue(prop.initializer);
}
return meta as unknown as PluginMeta;
}
function getCommentText(reflection: DeclarationReflection): string {
if (!reflection.comment) return '';
// Prefer summary text (text before any block tags)
const summary = reflection.comment.summary
.map((part) => part.text)
.join('')
.trim();
if (summary) return summary;
// Fall back to @description block tag (used by most plugins)
const descTag = reflection.comment.blockTags?.find((t: CommentTag) => t.tag === '@description');
if (descTag) {
return descTag.content
.map((part) => part.text)
.join('')
.trim();
}
return '';
}
function getTagValue(reflection: DeclarationReflection, tagName: string): string | undefined {
if (!reflection.comment) return undefined;
const tag = reflection.comment.blockTags?.find((t: CommentTag) => t.tag === `@${tagName}`);
if (!tag) return undefined;
return tag.content
.map((part) => part.text)
.join('')
.trim();
}
function generateReadme(name: string, pluginSlug: string, description: string, meta: PluginMeta): string {
const installCmd = meta.install ?? `ionic cordova plugin add ${meta.plugin ?? 'PLUGIN_NAME'}`;
const npmPkg = `@awesome-cordova-plugins/${pluginSlug}`;
let readme = `# ${name}\n\n`;
readme += '```\n';
readme += `$ ${installCmd}\n`;
readme += `$ npm install ${npmPkg}\n`;
readme += '```\n\n';
readme += `## [Usage Documentation](https://danielsogl.gitbook.io/awesome-cordova-plugins/plugins/${pluginSlug}/)\n\n`;
if (meta.repo) {
readme += `Plugin Repo: [${meta.repo}](${meta.repo})\n\n`;
}
if (description) {
readme += `${description}\n\n`;
}
if (meta.platforms && meta.platforms.length > 0) {
readme += '## Supported platforms\n\n';
for (const platform of meta.platforms) {
readme += `- ${platform}\n`;
}
readme += '\n';
}
return readme;
}
// Custom JSDoc block tags used by plugin source files (rendered in Gitbook)
const CUSTOM_BLOCK_TAGS = [
'@advanced',
'@author',
'@capacitorincompatible',
'@classes',
'@description',
'@enums',
'@interfaces',
'@Interfaces',
'@kind',
'@link',
'@name',
'@note',
'@paid',
'@platform',
'@premier',
'@return',
'@static',
'@usage',
'@warning',
] as const;
// TypeDoc default block tags + project-specific custom tags for Gitbook
const BLOCK_TAGS = [
'@param', '@returns', '@template', '@typeParam', '@module', '@inheritDoc',
'@group', '@category', '@categoryDescription', '@groupDescription',
'@defaultValue', '@default', '@example', '@remarks', '@see', '@throws',
'@since', '@deprecated', '@overload', '@enum', '@typedef', '@callback',
'@prop', '@property', '@satisfies', '@import', '@type', '@hidden',
'@ignore', '@internal', '@packageDocumentation', '@document', '@license',
'@private', '@protected', '@public',
...CUSTOM_BLOCK_TAGS,
] as const;
async function main(): Promise<void> {
// TypeDoc's CJS type exports map several option fields to incorrect types (e.g. `string` instead of
// `boolean` or `object`). The `unknown` cast works around this without affecting runtime behavior.
const app = await Application.bootstrapWithPlugins({
entryPoints: [`${PLUGINS_SRC}/*/index.ts`],
tsconfig: join(ROOT, 'tsconfig.json'),
skipErrorChecking: true,
logLevel: LogLevel.Error,
blockTags: [...BLOCK_TAGS],
inlineTags: ['@link', '@inheritDoc', '@label', '@code', '@type'],
validation: { notDocumented: false, invalidLink: false },
} as unknown as Parameters<typeof Application.bootstrapWithPlugins>[0]);
// Extract @Plugin() decorator metadata during TypeScript conversion
app.converter.on(
Converter.EVENT_CREATE_DECLARATION,
(context: Context, reflection: DeclarationReflection) => {
if (reflection.kind !== ReflectionKind.Class) return;
const symbol = context.getSymbolFromReflection(reflection);
if (!symbol) return;
const meta = extractPluginMeta(symbol);
if (meta) {
pluginMetaMap.set(reflection.id, meta);
}
}
);
const project: ProjectReflection | undefined = await app.convert();
if (!project) {
console.error('TypeDoc conversion failed');
process.exit(1);
}
// Copy root README to core docs
const coreDocsDir = join(DOCS_OUT, 'core');
mkdirSync(coreDocsDir, { recursive: true });
cpSync(join(ROOT, 'README.md'), join(coreDocsDir, 'README.md'));
let count = 0;
for (const reflection of project.getReflectionsByKind(ReflectionKind.Class)) {
const classRef = reflection as DeclarationReflection;
const meta = pluginMetaMap.get(classRef.id);
if (!meta || !meta.plugin) continue;
// Extract plugin slug from source file path
// Use @awesome-cordova-plugins/plugins/ prefix to avoid matching "awesome-cordova-plugins/src/"
const sourcePath = classRef.sources?.[0]?.fullFileName ?? '';
const slugMatch = sourcePath.match(/@awesome-cordova-plugins\/plugins\/([^/]+)\//);
if (!slugMatch) continue;
const pluginSlug = slugMatch[1];
const pluginName = getTagValue(classRef, 'name') ?? classRef.name;
const description = getCommentText(classRef);
const readmeContent = generateReadme(pluginName, pluginSlug, description, meta);
const outDir = join(DOCS_OUT, pluginSlug);
mkdirSync(outDir, { recursive: true });
writeFileSync(join(outDir, 'README.md'), readmeContent, 'utf-8');
count++;
}
console.log(`${count} README files generated`);
}
main().catch((err: unknown) => {
console.error(err);
process.exit(1);
});
-36
View File
@@ -1,36 +0,0 @@
'use strict';
const config = require('./config.json'),
projectPackage = require('../../package.json'),
path = require('path'),
fs = require('fs-extra'),
Dgeni = require('dgeni');
module.exports = (gulp) => {
gulp.task('docs', () => {
try {
const ionicPackage = require('./dgeni/dgeni-config')(projectPackage.version),
dgeni = new Dgeni([ionicPackage]);
return dgeni.generate().then((docs) => console.log(docs.length + ' docs generated'));
} catch (err) {
console.log(err.stack);
}
});
gulp.task('readmes', () => {
fs.copySync(
path.resolve(__dirname, '..', '..', 'README.md'),
path.resolve(__dirname, '..', '..', config.pluginDir, 'core', 'README.md')
);
try {
const ionicPackage = require('./dgeni/dgeni-readmes-config')(projectPackage.version),
dgeni = new Dgeni([ionicPackage]);
return dgeni.generate().then((docs) => {
console.log(docs.length + ' README files generated');
});
} catch (err) {
console.log(err.stack);
}
});
};