diff --git a/packages/uni-stacktracey/build.json b/packages/uni-stacktracey/build.json new file mode 100644 index 0000000000000000000000000000000000000000..ee5ec19d118d66fbff02a82317f5119f40d9276d --- /dev/null +++ b/packages/uni-stacktracey/build.json @@ -0,0 +1,9 @@ +[ + { + "input": { + "src/index.ts": [ + "dist/uni-stacktracey.cjs.js" + ] + } + } +] \ No newline at end of file diff --git a/packages/uni-stacktracey/dist/uni-stacktracey.cjs.js b/packages/uni-stacktracey/dist/uni-stacktracey.cjs.js new file mode 100644 index 0000000000000000000000000000000000000000..22121a4eaab359b57200988a83845c929282f258 --- /dev/null +++ b/packages/uni-stacktracey/dist/uni-stacktracey.cjs.js @@ -0,0 +1,209 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +var fs = require('fs'); +var StackTracey = require('stacktracey'); +var sourceMap = require('source-map'); +var axios = require('axios'); + +function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } + +var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); +var StackTracey__default = /*#__PURE__*/_interopDefaultLegacy(StackTracey); +var axios__default = /*#__PURE__*/_interopDefaultLegacy(axios); + +const isBrowser = typeof globalThis !== 'undefined' && globalThis.window === globalThis && globalThis.navigator; +const nixSlashes = (x) => x.replace(/\\/g, '/'); +const sourcemapCatch = {}; +if (isBrowser) { + // @ts-ignore + sourceMap.SourceMapConsumer.initialize({ + 'lib/mappings.wasm': 'https://unpkg.com/source-map@0.7.3/lib/mappings.wasm', + }); +} +function stacktracey(stacktrace, opts) { + const parseStack = []; + const stack = opts.preset.parseStacktrace(stacktrace); + stack.items.forEach((item, index) => { + const fn = () => { + const { line = 0, column = 0, file, fileName } = item; + let sourceMapUrl; + try { + sourceMapUrl = opts.preset.parseSourceMapUrl(file, fileName); + if (sourceMapUrl) { + return Promise.resolve(getSourceMapContent(sourceMapUrl)).then((content) => { + if (content) + return sourceMap.SourceMapConsumer.with(content, null, (consumer) => { + const sourceMapContent = parseSourceMapContent(consumer, { + line, + column, + }); + if (sourceMapContent) { + const { source, sourcePath, sourceLine, sourceColumn, fileName = '', } = sourceMapContent; + stack.items[index] = Object.assign({}, item, { + file: source, + line: sourceLine, + column: sourceColumn, + fileShort: sourcePath, + fileRelative: sourcePath, + fileName, + }); + } + }); + }); + } + return Promise.resolve(); + } + catch (error) { + return Promise.resolve(); + } + }; + parseStack.push(fn()); + }); + return new Promise((resolve, reject) => { + Promise.all(parseStack) + .then(() => { + const parseError = opts.preset.asTableStacktrace({ + maxColumnWidths: { + callee: 999, + file: 999, + sourceLine: 999, + }, + stacktrace, + }); + resolve(parseError); + }) + .catch(() => { + resolve(stacktrace); + }); + }); +} +function getSourceMapContent(sourcemapUrl) { + try { + return (sourcemapCatch[sourcemapUrl] || + (sourcemapCatch[sourcemapUrl] = new Promise((resolve, reject) => { + try { + if (/^[a-z]+:/i.test(sourcemapUrl)) { + axios__default["default"] + .get(sourcemapUrl) + .then((res) => { + console.log('sourcemapUrl :>> ', sourcemapUrl); + sourcemapCatch[sourcemapUrl] = res.data; + resolve(sourcemapCatch[sourcemapUrl]); + }) + .catch((_) => { + resolve(''); + }); + } + else { + sourcemapCatch[sourcemapUrl] = fs__default["default"].readFileSync(sourcemapUrl, 'utf-8'); + resolve(sourcemapCatch[sourcemapUrl]); + } + } + catch (error) { + resolve(''); + } + }))); + } + catch (error) { + return ''; + } +} +function parseSourceMapContent(consumer, obj) { + // source -> 'uni-app:///node_modules/@sentry/browser/esm/helpers.js' + const { source, line: sourceLine, column: sourceColumn, } = consumer.originalPositionFor(obj); + if (source) { + const sourcePathSplit = source.split('/'); + const sourcePath = sourcePathSplit.slice(3).join('/'); + const fileName = sourcePathSplit.pop(); + return { + source, + sourcePath, + sourceLine: sourceLine === null ? 0 : sourceLine, + sourceColumn: sourceColumn === null ? 0 : sourceColumn, + fileName, + }; + } +} +function uniStracktraceyPreset(opts) { + const { base, platform, version } = opts; + let stack; + return { + parseSourceMapUrl(file, fileName) { + if (!platform || !version) + return ''; + // 根据 base,platform,version,filename 组合 sourceMapUrl + return `${base}/${version}/.sourcemap/${platform}/${file.split('.')[0]}.js.map`; + }, + parseStacktrace(stacktrace) { + return (stack = new StackTracey__default["default"](stacktrace)); + }, + asTableStacktrace({ maxColumnWidths, stacktrace } = { stacktrace: '' }) { + const errorName = stacktrace.split('\n')[0]; + return errorName.indexOf('at') === -1 + ? `${errorName}\n` + : '' + (stack.asTable ? stack.asTable({ maxColumnWidths }) : ''); + }, + }; +} +function utsStracktraceyPreset(opts) { + let stack; + return { + parseSourceMapUrl(file, fileName) { + // 根据 base,filename 组合 sourceMapUrl + return `${opts.base}/${fileName}.map`; + }, + parseStacktrace(str) { + const lines = (str || '').split('\n'); + const entries = lines + .map((line) => { + line = line.trim(); + let callee, fileLineColumn = [], planA, planB; + if ((planA = line.match(/e: \[(.+)\](.+): (.+)/))) { + callee = planA[1]; + fileLineColumn = (planA[2].match(/(.+):.*\((\d+).+?(\d+)\)/) || []).slice(1); + } + else { + return undefined; + } + const fileName = fileLineColumn[0] + ? (planB = fileLineColumn[0].match(/(\/.*)*\/(.+)/) || [])[2] || '' + : ''; + return { + beforeParse: line, + callee: callee || '', + index: false, + native: false, + file: nixSlashes(fileLineColumn[0] || ''), + line: parseInt(fileLineColumn[1] || '', 10) || undefined, + column: parseInt(fileLineColumn[2] || '', 10) || undefined, + fileName, + fileShort: planB ? planB[1] : '', + errMsg: planA[3] || '', + calleeShort: '', + fileRelative: '', + thirdParty: false, + }; + }) + .filter((x) => x !== undefined); + return (stack = { + items: entries, + }); + }, + asTableStacktrace({ stacktrace } = { stacktrace: '' }) { + const stacktraceSplit = stacktrace.split('\n'); + const errorName = stacktraceSplit[0]; + const errorMsg = stacktraceSplit.pop(); + return ((errorName.indexOf('e:') === -1 ? `${errorName}\n` : '') + + (stack.items + .map((item) => `e: [${item.callee}]${item.fileShort}/${item.fileName}: (${item.line}, ${item.column}): ${item.errMsg}`) + .join('\n') + + (errorMsg ? `\n\n${errorMsg}` : ''))); + }, + }; +} + +exports.stacktracey = stacktracey; +exports.uniStracktraceyPreset = uniStracktraceyPreset; +exports.utsStracktraceyPreset = utsStracktraceyPreset; diff --git a/packages/uni-stacktracey/package.json b/packages/uni-stacktracey/package.json new file mode 100644 index 0000000000000000000000000000000000000000..04b554202ae4a4bfc42708205f9a96b64f2a844d --- /dev/null +++ b/packages/uni-stacktracey/package.json @@ -0,0 +1,28 @@ +{ + "name": "@dcloudio/uni-stacktracey", + "version": "3.0.0-alpha-3040820220424005", + "description": "@dcloudio/uni-stacktracey", + "main": "dist/uni-stacktracey.es.js", + "module": "dist/uni-stacktracey.es.js", + "files": [ + "dist", + "lib" + ], + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/dcloudio/uni-app.git", + "directory": "packages/uni-stacktracey" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/dcloudio/uni-app/issues" + }, + "gitHead": "33e807d66e1fe47e2ee08ad9c59247e37b8884da", + "dependencies": { + "stacktracey": "^2.1.8", + "source-map": "^0.7.3", + "axios": "^0.27.2" + }, + "devDependencies": {} +} \ No newline at end of file diff --git a/packages/uni-stacktracey/src/index.ts b/packages/uni-stacktracey/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6507d9b7aa2983cca5aad48b34e3c57dc2ee3415 --- /dev/null +++ b/packages/uni-stacktracey/src/index.ts @@ -0,0 +1,304 @@ +import fs from 'fs' +import StackTracey from 'stacktracey' +import { + SourceMapConsumer, + BasicSourceMapConsumer, + IndexedSourceMapConsumer, + Position, +} from 'source-map' +import axios from 'axios' + +const nixSlashes = (x: string) => x.replace(/\\/g, '/') +const sourcemapCatch: Record> = {} + +type StacktraceyItems = StackTracey.Entry & { + errMsg?: string +} +type Stacktracey = { + items: StacktraceyItems[] + asTable?: StackTracey['asTable'] +} +interface StacktraceyPreset { + /** + * 解析错误栈信息 + * @param filename + */ + parseStacktrace(stacktrace: string): Stacktracey + /** + * 根据错误信息重新赋值为错误栈信息 + * @param filename + */ + asTableStacktrace(opts?: { + maxColumnWidths?: StackTracey.MaxColumnWidths + stacktrace: string + }): string + /** + * 根据编译后的文件名地址 + * @param file + * 根据编译后的文件名 + * @param filename + */ + parseSourceMapUrl(file: string, fileName: string): string +} + +interface StacktraceyOptions { + preset: StacktraceyPreset +} + +export function stacktracey( + stacktrace: string, + opts: StacktraceyOptions +): Promise { + const parseStack: Array> = [] + + const stack = opts.preset.parseStacktrace(stacktrace) + + stack.items.forEach((item, index) => { + const fn = () => { + const { line = 0, column = 0, file, fileName } = item + let sourceMapUrl + try { + sourceMapUrl = opts.preset.parseSourceMapUrl(file, fileName) + if (sourceMapUrl) { + return Promise.resolve(getSourceMapContent(sourceMapUrl)).then( + (content) => { + if (content) + return SourceMapConsumer.with(content, null, (consumer) => { + const sourceMapContent = parseSourceMapContent(consumer, { + line, + column, + }) + + if (sourceMapContent) { + const { + source, + sourcePath, + sourceLine, + sourceColumn, + fileName = '', + } = sourceMapContent + + stack.items[index] = Object.assign({}, item, { + file: source, + line: sourceLine, + column: sourceColumn, + fileShort: sourcePath, + fileRelative: sourcePath, + fileName, + }) + } + }) + } + ) + } + return Promise.resolve() + } catch (error) { + return Promise.resolve() + } + } + parseStack.push(fn()) + }) + + return new Promise((resolve, reject) => { + Promise.all(parseStack) + .then(() => { + const parseError = opts.preset.asTableStacktrace({ + maxColumnWidths: { + callee: 999, + file: 999, + sourceLine: 999, + }, + stacktrace, + }) + resolve(parseError) + }) + .catch(() => { + resolve(stacktrace) + }) + }) +} + +function getSourceMapContent(sourcemapUrl: string) { + try { + return ( + sourcemapCatch[sourcemapUrl] || + (sourcemapCatch[sourcemapUrl] = new Promise((resolve, reject) => { + try { + if (/^[a-z]+:/i.test(sourcemapUrl)) { + axios + .get(sourcemapUrl) + .then((res) => { + console.log('sourcemapUrl :>> ', sourcemapUrl) + sourcemapCatch[sourcemapUrl] = res.data + resolve(sourcemapCatch[sourcemapUrl]) + }) + .catch((_) => { + resolve('') + }) + } else { + sourcemapCatch[sourcemapUrl] = fs.readFileSync( + sourcemapUrl, + 'utf-8' + ) + resolve(sourcemapCatch[sourcemapUrl]) + } + } catch (error) { + resolve('') + } + })) + ) + } catch (error) { + return '' + } +} + +type SourceMapContent = + | undefined + | { + source: string + sourcePath: string + sourceLine: number + sourceColumn: number + fileName: string | undefined + } +function parseSourceMapContent( + consumer: BasicSourceMapConsumer | IndexedSourceMapConsumer, + obj: Position +): SourceMapContent { + // source -> 'uni-app:///node_modules/@sentry/browser/esm/helpers.js' + const { + source, + line: sourceLine, + column: sourceColumn, + } = consumer.originalPositionFor(obj) + if (source) { + const sourcePathSplit = source.split('/') + const sourcePath = sourcePathSplit.slice(3).join('/') + const fileName = sourcePathSplit.pop() + + return { + source, + sourcePath, + sourceLine: sourceLine === null ? 0 : sourceLine, + sourceColumn: sourceColumn === null ? 0 : sourceColumn, + fileName, + } + } +} + +interface UniStracktraceyPresetOptions { + base: string + appId: string + platform: string + version: string +} +export function uniStracktraceyPreset( + opts: UniStracktraceyPresetOptions +): StacktraceyPreset { + const { base, platform, version } = opts + + let stack: Stacktracey + + return { + parseSourceMapUrl(file, fileName) { + if (!platform || !version) return '' + // 根据 base,platform,version,filename 组合 sourceMapUrl + return `${base}/${version}/.sourcemap/${platform}/${ + file.split('.')[0] + }.js.map` + }, + parseStacktrace(stacktrace) { + return (stack = new StackTracey(stacktrace)) + }, + asTableStacktrace({ maxColumnWidths, stacktrace } = { stacktrace: '' }) { + const errorName = stacktrace.split('\n')[0] + return errorName.indexOf('at') === -1 + ? `${errorName}\n` + : '' + (stack.asTable ? stack.asTable({ maxColumnWidths }) : '') + }, + } +} + +interface UtsStracktraceyPreset { + /** + * source 根目录(如:/wgtRoot/__UNI__E070870/nativeplugins/DCloud-UTSPlugin/android/src/) + */ + sourceRoot: string + /** + * sourceMap 根目录 + */ + base: string +} +export function utsStracktraceyPreset( + opts: UtsStracktraceyPreset +): StacktraceyPreset { + let stack: Stacktracey + return { + parseSourceMapUrl(file, fileName) { + // 根据 base,filename 组合 sourceMapUrl + return `${opts.base}/${fileName}.map` + }, + parseStacktrace(str) { + const lines = (str || '').split('\n') + + const entries = lines + .map((line) => { + line = line.trim() + + let callee, + fileLineColumn = [], + planA, + planB + + if ((planA = line.match(/e: \[(.+)\](.+): (.+)/))) { + callee = planA[1] + fileLineColumn = ( + planA[2].match(/(.+):.*\((\d+).+?(\d+)\)/) || [] + ).slice(1) + } else { + return undefined + } + + const fileName = fileLineColumn[0] + ? (planB = fileLineColumn[0].match(/(\/.*)*\/(.+)/) || [])[2] || '' + : '' + + return { + beforeParse: line, + callee: callee || '', + index: false, + native: false, + file: nixSlashes(fileLineColumn[0] || ''), + line: parseInt(fileLineColumn[1] || '', 10) || undefined, + column: parseInt(fileLineColumn[2] || '', 10) || undefined, + fileName, + fileShort: planB ? planB[1] : '', + errMsg: planA[3] || '', + calleeShort: '', + fileRelative: '', + thirdParty: false, + } + }) + .filter((x) => x !== undefined) + + return (stack = { + items: entries as StackTracey.Entry[], + }) + }, + asTableStacktrace({ stacktrace } = { stacktrace: '' }) { + const stacktraceSplit = stacktrace.split('\n') + const errorName = stacktraceSplit[0] + const errorMsg = stacktraceSplit.pop() + return ( + (errorName.indexOf('e:') === -1 ? `${errorName}\n` : '') + + (stack.items + .map( + (item) => + `e: [${item.callee}]${item.fileShort}/${item.fileName}: (${item.line}, ${item.column}): ${item.errMsg}` + ) + .join('\n') + + (errorMsg ? `\n\n${errorMsg}` : '')) + ) + }, + } +} diff --git a/packages/uni-stacktracey/test/index.js b/packages/uni-stacktracey/test/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6c8c470c48ca89be76a8c399424d897db8bb3f78 --- /dev/null +++ b/packages/uni-stacktracey/test/index.js @@ -0,0 +1,38 @@ +const { + stacktracey, + uniStracktraceyPreset, + utsStracktraceyPreset, +} = require('../dist/uni-stacktracey.cjs.js') + +const utsErrorMsg = `Appid: __UNI__E070870 +e: [PackagePath]/wgtRoot/__UNI__E070870/nativeplugins/DCloud-UTSPlugin/android/src/io/dcloud/uniplugin/TestModule.kt: (8, 1): Expecting a top level declaration +e: [PackagePath]/wgtRoot/__UNI__E070870/nativeplugins/DCloud-UTSPlugin/android/src/io/dcloud/uniplugin/TestModule.kt: (8, 9): Expecting a top level declaration +e: [PackagePath]/wgtRoot/__UNI__E070870/nativeplugins/DCloud-UTSPlugin/android/src/io/dcloud/uniplugin/TestModule.kt: (8, 10): Expecting a top level declaration +e: [PackagePath]/wgtRoot/__UNI__E070870/nativeplugins/DCloud-UTSPlugin/android/src/io/dcloud/uniplugin/TestModule.kt: (8, 11): Expecting a top level declaration +e: [PackagePath]/wgtRoot/__UNI__E070870/nativeplugins/DCloud-UTSPlugin/android/src/io/dcloud/uniplugin/TestModule.kt: (8, 16): Expecting a top level declaration + +FAILURE: Build failed with an exception.` + +/* stacktracey( + `ReferenceError: Sentry is not defined + at Proxy.throwError(/vue3/assets/pages-index-index.4077a069.js:1:295) + at e(/vue3/assets/index.2be9343a.js:1:52317) + at Ue(/vue3/assets/index.2be9343a.js:1:16271) + at He(/vue3/assets/index.2be9343a.js:1:16349) + at HTMLElement.n(/vue3/assets/index.2be9343a.js:1:51834) + at HTMLElement.o(/vue3/assets/index.2be9343a.js:21:60087)`, + { + preset: uniStracktraceyPreset({ + base: 'https://7463-tcb-uzyfn59tqxjxtnbab2e2c-5ba40b-1303909289.tcb.qcloud.la', + platform: 'h5', + version: '1.0.0', + }), + } +).then((res) => console.log(res)) */ + +stacktracey(utsErrorMsg, { + preset: utsStracktraceyPreset({ + base: '/wgtRoot/__UNI__E070870/nativeplugins/DCloud-UTSPlugin/android/src/io/dcloud/uniplugin', + sourceRoot: '', + }), +}).then((res) => console.log(res)) diff --git a/packages/uni-stacktracey/tsconfig.json b/packages/uni-stacktracey/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..cc86ba150c0969e927a8da52313d067173a2716c --- /dev/null +++ b/packages/uni-stacktracey/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "outDir": "dist", + "sourceMap": false, + "target": "es2015", + "module": "esnext", + "moduleResolution": "node", + "allowJs": false, + "strict": true, + "noUnusedLocals": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "removeComments": false, + "lib": [ + "ESNext", + "DOM" + ] + }, + "include": [ + "src", + "node_modules/source-map/source-map.d.ts" + ] +} \ No newline at end of file