diff --git a/build-tools/jsdoc_format_plugin/entry.js b/build-tools/jsdoc_format_plugin/entry.js new file mode 100644 index 0000000000000000000000000000000000000000..4658722b36f98cc5028fee2a31bba0cc7eb6eba3 --- /dev/null +++ b/build-tools/jsdoc_format_plugin/entry.js @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2021-2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ \ No newline at end of file diff --git a/build-tools/jsdoc_format_plugin/jsdoc_rules.js b/build-tools/jsdoc_format_plugin/jsdoc_rules.js new file mode 100644 index 0000000000000000000000000000000000000000..57aea89e4be61b24e455c9aaabc91f0c2eb3373c --- /dev/null +++ b/build-tools/jsdoc_format_plugin/jsdoc_rules.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021-2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const decoratorSort = [ + ['description'], + ['@namespace', '@extends', '@typedef', '@interface', '@permission', '@enum', '@constant'], + ['@type', '@param', '@default'], + ['@returns'], + ['@readonly', '@throws'], + ['@static'], + ['@fires'], + ['@syscap', '@systemapi', '@famodeonly', '@stagemodeonly', '@crossplatform', '@since', '@deprecated', '@useinstead', '@example'] +] +exports.DECORATOR_SORT_LIST = decoratorSort; \ No newline at end of file diff --git a/build-tools/jsdoc_format_plugin/package.json b/build-tools/jsdoc_format_plugin/package.json new file mode 100644 index 0000000000000000000000000000000000000000..f4597822e4b77a4c9ab5771bab991853d636e7db --- /dev/null +++ b/build-tools/jsdoc_format_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "jsdoc_format_plugin", + "version": "1.0.0", + "description": "", + "main": "collect.js", + "scripts": { + "test": "cd test && node test.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "exceljs": "^4.3.0", + "fs": "^0.0.1-security", + "path": "^0.12.7", + "typescript": "^4.7.4" + } +} diff --git a/build-tools/jsdoc_format_plugin/plugin/api_file_path.txt b/build-tools/jsdoc_format_plugin/plugin/api_file_path.txt new file mode 100644 index 0000000000000000000000000000000000000000..61de004ae2588ad33a58f75015ef9dcde630279b --- /dev/null +++ b/build-tools/jsdoc_format_plugin/plugin/api_file_path.txt @@ -0,0 +1,2 @@ +# api absolute path +Z:\root\interface\sdk-js\api\xxx.d.ts \ No newline at end of file diff --git a/build-tools/jsdoc_format_plugin/src/format_logs.js b/build-tools/jsdoc_format_plugin/src/format_logs.js new file mode 100644 index 0000000000000000000000000000000000000000..4afe20eefe8687c8ac9aebd30d3863bde73ab1cf --- /dev/null +++ b/build-tools/jsdoc_format_plugin/src/format_logs.js @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2021-2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/build-tools/jsdoc_format_plugin/src/jsdoc_format_plugin.js b/build-tools/jsdoc_format_plugin/src/jsdoc_format_plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..c2eb9e4473ba81183caf3f94501af05562b54c4c --- /dev/null +++ b/build-tools/jsdoc_format_plugin/src/jsdoc_format_plugin.js @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2021-2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const path = require('path'); +const ts = require('typescript'); +const { overwriteApiFile, getApiFiles, tsTransform, hasAPINote, getApiInfo, JSDOC_WRITELIST_SET, getAPINote, + hasCopyright, getParentApiInfo, TS_KEYWORD_SET } = require('./utils'); + +const formatedNodes = new Set([]); +let copyrightMessage = ""; + +function formatApiFile(url) { + return (context) => { + return (node) => { + sourceFile = node; + const fileName = url.replace(/\.d\.ts$/, ".new.d.ts"); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const result = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); + overwriteApiFile(fileName, result, {}); + tsTransform([fileName], formatJsDoc); + return node; + } + } +} + +function checkJsDoc(node) { + if (hasAPINote(node)) { + if (!getApiInfo(node).version) { + return false; + } + } + return true; +} + +function createNewJsdoc(node, docContent) { + // step1. get api information + let hasCR = false; + if (hasCopyright(docContent)) { + contents = docContent.split(/\*\//); + hasCR = true; + if (hasCopyright(contents[0])) { + copyrightMessage = contents[0] + "*/"; + } + docContent = docContent.replace(copyrightMessage, ""); + } + // 根据* @拆分jsDoc + const docs = docContent.split(/ *\* *\@/); + + let docInfo = {}; + docs.forEach((doc, docsIndex) => { + const lineInfos = doc.split(/[(\r\n)\r\n]+/); + let decoratorContent = ""; + lineInfos.forEach((lineInfo) => { + const content = lineInfo.replace(/( *\/\*\* *)|( *\*\/ *)|( *\* *)/g, ""); + if (content !== "") { + decoratorContent += `${content} `; + } + }); + decoratorContent = decoratorContent.trim(); + if (docsIndex === 0) { + docInfo["description"] = decoratorContent; + // 待补充全量报错场景 + if (ts.isConstructorDeclaration(node)) { + docInfo["apiName"] = "constructor"; + } else { + if (!node.name) { + console.log(node.getFullText()) + } + docInfo["apiName"] = node.name.escapedText.toString(); + } + } else { + const decoratorName = decoratorContent.match(/^[a-z]+\b/)[0]; + if (JSDOC_WRITELIST_SET.has(decoratorName)) { + if (!docInfo[decoratorName]) { + docInfo[decoratorName] = [decoratorContent.replace(`${decoratorName} `, "")]; + } else { + docInfo[decoratorName] = docInfo[decoratorName].push(decoratorContent.replace(`${decoratorName} `, "")); + } + } + } + }); + docInfo = getParentApiInfo(node, docInfo); + // 缩进空格 + const indentations = docContent.match(/(?<=\n) *(?=$)/g)[0]; + + // step2. create jsdoc + let newNodeFullText = ""; + if (ts.isModuleDeclaration(node)) { + const docSystemapi = docInfo.systemapi && docInfo.systemapi[0] ? `${indentations + " * "}@systemapi\n` : ""; + const docModel = docInfo.model && docInfo.model[0] ? `${indentations + " * "}@${docInfo.model[0]}\n` : ""; + const docCrossplatform = docInfo.crossplatform && docInfo.crossplatform[0] ? + `${indentations + " * "}@crossplatform\n` : ""; + const docDeprecated = docInfo.deprecated && docInfo.deprecated[0] ? + `${indentations + " * "}@deprecated ${docInfo.deprecated[0]}\n` : ""; + const docUseinstead = docInfo.useinstead && docInfo.useinstead[0] ? + `${indentations + " * "}@useinstead ${docInfo.useinstead[0]}\n` : ""; + const docPermission = docInfo.permission && docInfo.permission[0] ? + `${indentations + " * "}@useinstead ${docInfo.permission[0]}\n` : ""; + + newNodeFullText = `${indentations}/**\n` + + `${indentations + " * "}${docInfo.description}\n` + + `${indentations + " * "}@namespace ${docInfo.namespace ? docInfo.namespace : docInfo.apiName}\n` + + docPermission + + `${indentations + " * "}@syscap ${docInfo.syscap}\n` + + docSystemapi + docModel + docCrossplatform + + `${indentations + " * "}@since ${docInfo.since}\n` + + docDeprecated + docUseinstead + + `${indentations + " */"}\n${indentations}`; + if (hasCR) { + newNodeFullText = copyrightMessage + "\n" + newNodeFullText + node.getText(); + } + } + return newNodeFullText; +} + +function formatJsDoc(fileName) { + return (context) => { + return (node) => { + sourceFile = node; + node = ts.visitEachChild(node, processAllNodes, context); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const result = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); + overwriteApiFile(fileName, result, {}); + return node; + } + function processAllNodes(node) { + if (hasAPINote(node) && !formatedNodes.has(fileName + node.pos + node.end) && !TS_KEYWORD_SET.has(node.kind)) { + formatedNodes.add(fileName + node.pos + node.end); + if (ts.isModuleDeclaration(node)) { + const result = createNewJsdoc(node, getAPINote(node)); + console.log(result); + } + } + return ts.visitEachChild(node, processAllNodes, context); + } + } +} + +function main(apiDir) { + const apiFiles = getApiFiles(apiDir); + tsTransform(apiFiles, formatApiFile); +} + +const testDir = path.resolve(__dirname, "../plugin/api_file_path.txt"); +main(testDir); diff --git a/build-tools/jsdoc_format_plugin/src/utils.js b/build-tools/jsdoc_format_plugin/src/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..7f4f42411c908d11db22356ca4322cda9d27e32d --- /dev/null +++ b/build-tools/jsdoc_format_plugin/src/utils.js @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2021-2022 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +/** + * Recursively query the api directory. + * @param {String} dir dir api directory + * @param {Array} apiFiles api files array + */ +function readDir(dir, apiFiles) { + const files = fs.readFileSync(dir); + files.forEach(element => { + const filePath = path.join(dir, element); + const status = fs.statSync(filePath); + if (status.isDirectory()) { + readDir(filePath, apiFiles); + } else if (status.isFile() && /\.d\.ts$/.test(filePath)) { + apiFiles.push(filePath); + } + }); +} + +/** + * Get api file array. + * @param {String} url api file(.d.ts) | api config file(.txt) | api file directory + * @returns api file array + */ +function getApiFiles(url) { + const apiFiles = []; + const urlStat = fs.statSync(url); + if (urlStat.isFile() && path.extname(url) === ".txt") { + const content = fs.readFileSync(url, "utf-8"); + const apiFilesConfig = content.split(/[(\r\n)\r\n]+/); + apiFilesConfig.forEach(file => { + if (fs.existsSync(file)) { + apiFiles.push(file); + } else { + console.error(`FORMAT_ERROR: Api file of [${file}] is not found!`); + } + }); + } else if (urlStat.isFile() && /\.d\.ts$/.test(url)) { + apiFiles.push(url); + } else if (url.isDirectory()) { + readDir(url, apiFiles); + } + return apiFiles; +} +exports.getApiFiles = getApiFiles; + +/** + * Tsc compile engine. + * @param {Array} apiFiles api file array + * @param {Function} callback pretreatment function + */ +function tsTransform(apiFiles, callback) { + apiFiles.forEach(url => { + const content = fs.readFileSync(url, "utf-8"); + const fileName = path.basename(url).replace(/\.d\.ts$/, "ts"); + ts.transpileModule(content, { + compilerOptions: { + "target": ts.ScriptTarget.ES2017 + }, + fileName: fileName, + transformers: { before: [callback(url)] } + }); + }); +} +exports.tsTransform = tsTransform; + +/** + * Create generated api file. + * @param {String} url generated api file path + * @param {String} content content generated api file content + * @param {Object} option option of writeFile(fs) + */ +function overwriteApiFile(url, content, option) { + fs.writeFileSync(url, content, option); +} +exports.overwriteApiFile = overwriteApiFile; + +/** + * Get api note. + * @param {tsNode} node current node + * @returns api note + */ +function getAPINote(node) { + const apiLength = node.getText().length; + const apiFullLength = node.getFullText().length + return node.getFullText().substring(0, apiFullLength - apiLength); +} +exports.getAPINote = getAPINote; + +/** + * Judge whether the api note exists. + * @param {tsNode} node current node + * @returns boolean + */ +function hasAPINote(node) { + const apiNote = getAPINote(node).replace(/[\s]/g, ""); + if (apiNote && apiNote.length !== 0) { + return true; + } + return false; +} +exports.hasAPINote = hasAPINote; + +/** + * + * @param {tsNode} node current node + * @returns boolean + */ +function hasCopyright(content) { + return /http\:\/\/www\.apache\.org\/licenses\/LICENSE\-2\.0/g.test(content); +} +exports.hasCopyright = hasCopyright; + +/** + * Get api information. + * @param {tsNode} node current node + * @returns api information + */ +function getApiInfo(node) { + const notesStr = getAPINote(node) + let apiInfo = {}; + if (notesStr !== "") { + if (/\@systemapi/g.test(notesStr)) { + apiInfo.systemapi = true; + } + if (/\@since\s*(\d+)/g.test(notesStr)) { + notesStr.replace(/\@since\s*(\d+)/g, (versionInfo) => { + apiInfo.since = versionInfo.replace(/\@since/g, '').trim(); + }) + } + if (/\@deprecated.*since\s*(\d+)/g.test(notesStr)) { + notesStr.replace(/\@deprecated.*since\s*(\d+)/g, + versionInfo => { + apiInfo.deprecated = versionInfo.replace( + /\@deprecated.*since\s*/g, '').trim(); + }) + } + if (/\@famodelonly/g.test(notesStr)) { + apiInfo.model = "famodelonly"; + } else if (/\@stagemodelonly/g.test(notesStr)) { + apiInfo.model = "stagemodelonly"; + } + if (/\@syscap\s*((\w|\.|\/|\{|\@|\}|\s)+)/g.test(notesStr)) { + notesStr.replace(/\@syscap\s*((\w|\.|\/|\{|\@|\}|\s)+)/g, sysCapInfo => { + apiInfo.syscap = sysCapInfo.replace(/\@syscap/g, '').trim(); + }) + } + if (/\@permission\s*((\w|\.|\/|\{|\@|\}|\s)+)/g.test(notesStr)) { + notesStr.replace(/\@permission\s*((\w|\.|\/|\{|\@|\}|\s)+)/g, + permissionInfo => { + apiInfo.permission = + permissionInfo.replace(/\@permission/g, '').trim(); + }) + } + } + return apiInfo; +} +exports.getApiInfo = getApiInfo; + +function getParentApiInfo(node, apiInfo) { + if (!apiInfo.since) { + if (getApiInfo(node).since) { + apiInfo.since = [getApiInfo(node).since]; + } else if (node.parent) { + apiInfo.since = [getParentApiInfo(node.parent, apiInfo).since]; + } else { + apiInfo.since = ["NA"]; + } + } + if (!apiInfo.model) { + if (getApiInfo(node).model) { + apiInfo.model = [getApiInfo(node).model]; + } else if (node.parent) { + apiInfo.model = [getParentApiInfo(node.parent, apiInfo).model]; + } + } + if (!apiInfo.systemapi) { + if (getApiInfo(node).systemapi) { + apiInfo.model = [getApiInfo(node).systemapi]; + } else if (node.parent) { + apiInfo.model = [getParentApiInfo(node.parent, apiInfo).systemapi]; + } else { + apiInfo.systemapi = [false]; + } + } + if (!apiInfo.syscap) { + if (getApiInfo(node).syscap) { + apiInfo.model = [getApiInfo(node).syscap]; + } else if (node.parent) { + apiInfo.model = [getParentApiInfo(node.parent, apiInfo).syscap]; + } else { + apiInfo.systemapi = ["NA"]; + } + } + return apiInfo; +} +exports.getParentApiInfo = getParentApiInfo; + +const JSDOC_WRITELIST_SET = new Set(["constant", "crossplatform", "default", "deprecated", "enum", "example", "extends", + "famodeonly", "fires", "interface", "namespace", "param", "permission", "readonly", "returns", "since", "stagemodeonly", + "static", "syscap", "systemapi", "type", "typedef", "throws", "test", "useinstead"]); +exports.JSDOC_WRITELIST_SET = JSDOC_WRITELIST_SET; + +const TS_KEYWORD_SET = new Set([ts.SyntaxKind.DeclareKeyword]); +exports.TS_KEYWORD_SET = TS_KEYWORD_SET;