From 744504652c06bbbe1718edce5729bb7af1431135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=AA=E8=A7=89=E9=9B=A8=E5=A3=B0?= <544022268@qq.com> Date: Sat, 15 Jul 2023 01:23:52 +0800 Subject: [PATCH] feat: support sourcemap to `.vue` files (#243) * wip: effective .vue source map * wip: process sources' paths * chore: update example --- examples/vue/components/TypeProps.vue | 10 +++--- examples/vue/vite.config.ts | 13 ++++++-- src/plugin.ts | 37 ++++++++++++++++++++-- src/resolvers/vue.ts | 38 ++++++++++++++++++++-- src/utils.ts | 45 +++++++++++++++++++++++++++ tests/utils.spec.ts | 43 +++++++++++++++++++++++++ 6 files changed, 174 insertions(+), 12 deletions(-) diff --git a/examples/vue/components/TypeProps.vue b/examples/vue/components/TypeProps.vue index f1f2ca9..bc074a3 100644 --- a/examples/vue/components/TypeProps.vue +++ b/examples/vue/components/TypeProps.vue @@ -1,5 +1,9 @@ + + - - diff --git a/examples/vue/vite.config.ts b/examples/vue/vite.config.ts index 3f071f1..03ed92e 100644 --- a/examples/vue/vite.config.ts +++ b/examples/vue/vite.config.ts @@ -30,12 +30,19 @@ export default defineConfig({ plugins: [ dts({ copyDtsFiles: true, - outDir: ['dist', 'types'], + outDir: [ + 'dist', + 'types' + // 'types/inner' + ], // include: ['src/index.ts'], exclude: ['src/ignore'], staticImport: true, - rollupTypes: true, - insertTypesEntry: true + // rollupTypes: true, + insertTypesEntry: true, + compilerOptions: { + declarationMap: true + } }), vue(), vueJsx() diff --git a/src/plugin.ts b/src/plugin.ts index f71ffdd..274f1eb 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -485,6 +485,7 @@ export function dtsPlugin(options: PluginOptions = {}): import('vite').Plugin { Array.from(outputFiles.entries()), async ([path, content]) => { const isMapFile = path.endsWith('.map') + const baseDir = dirname(path) if (!isMapFile && content) { content = clearPureImport ? removePureImport(content) : content @@ -498,6 +499,19 @@ export function dtsPlugin(options: PluginOptions = {}): import('vite').Plugin { ) content = cleanVueFileName ? content.replace(/['"](.+)\.vue['"]/g, '"$1"') : content + if (isMapFile) { + try { + const sourceMap: { sources: string[] } = JSON.parse(content) + + sourceMap.sources = sourceMap.sources.map(source => { + return normalizePath(relative(dirname(path), resolve(baseDir, source))) + }) + content = JSON.stringify(sourceMap) + } catch (e) { + logger.warn(`${logPrefix} ${yellow('Processing source map fail:')} ${path}`) + } + } + await writeOutput(path, content, outDir) } ) @@ -624,8 +638,27 @@ export function dtsPlugin(options: PluginOptions = {}): import('vite').Plugin { const relativePath = relative(outDir, wroteFile) await Promise.all( - extraOutDirs.map(async outDir => { - await writeOutput(resolve(outDir, relativePath), content, outDir, false) + extraOutDirs.map(async targetOutDir => { + const path = resolve(targetOutDir, relativePath) + + if (wroteFile.endsWith('.map')) { + const relativeOutDir = relative(outDir, targetOutDir) + + if (relativeOutDir) { + try { + const sourceMap: { sources: string[] } = JSON.parse(content) + + sourceMap.sources = sourceMap.sources.map(source => { + return normalizePath(relative(relativeOutDir, source)) + }) + content = JSON.stringify(sourceMap) + } catch (e) { + logger.warn(`${logPrefix} ${yellow('Processing source map fail:')} ${path}`) + } + } + } + + await writeOutput(path, content, targetOutDir, false) }) ) }) diff --git a/src/resolvers/vue.ts b/src/resolvers/vue.ts index 75bab63..81ffeee 100644 --- a/src/resolvers/vue.ts +++ b/src/resolvers/vue.ts @@ -1,5 +1,12 @@ +import { base64VLQEncode } from '../utils' + import type { Resolver } from '../types' +interface SourceMap { + sources: string[], + mappings: string +} + const vueRE = /\.vue$/ export function VueResolver(): Resolver { @@ -8,7 +15,7 @@ export function VueResolver(): Resolver { supports(id) { return vueRE.test(id) }, - transform({ id, program, service }) { + transform({ id, code, program, service }) { const sourceFile = program.getSourceFile(id) || program.getSourceFile(id + '.ts') || @@ -18,12 +25,39 @@ export function VueResolver(): Resolver { if (!sourceFile) return [] - return service.getEmitOutput(sourceFile.fileName, true).outputFiles.map(file => { + const outputs = service.getEmitOutput(sourceFile.fileName, true).outputFiles.map(file => { return { path: file.name, content: file.text } }) + + if (!program.getCompilerOptions().declarationMap) return outputs + + const [beforeScript] = code.split(/\s*/) + const beforeLines = beforeScript.split('\n').length + + for (const output of outputs) { + if (output.path.endsWith('.map')) { + try { + const sourceMap: SourceMap = JSON.parse(output.content) + + sourceMap.sources = sourceMap.sources.map(source => + source.replace(/\.vue\.ts$/, '.vue') + ) + + if (beforeScript && beforeScript !== code && beforeLines) { + sourceMap.mappings = `${base64VLQEncode([0, 0, beforeLines, 0])};${ + sourceMap.mappings + }` + } + + output.content = JSON.stringify(sourceMap) + } catch (e) {} + } + } + + return outputs } } } diff --git a/src/utils.ts b/src/utils.ts index 48f8338..38173ca 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -234,6 +234,51 @@ export function getTsConfig( return tsConfig } +/** + * @see https://github.com/mozilla/source-map/blob/master/lib/base64-vlq.js + */ + +const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('') + +function base64Encode(number: number) { + if (number >= 0 && number < BASE64_ALPHABET.length) { + return BASE64_ALPHABET[number] + } + + throw new TypeError('Base64 integer must be between 0 and 63: ' + number) +} + +const VLQ_BASE_SHIFT = 5 +const VLQ_BASE = 1 << VLQ_BASE_SHIFT +const VLQ_BASE_MASK = VLQ_BASE - 1 +const VLQ_CONTINUATION_BIT = VLQ_BASE + +function toVLQSigned(number: number) { + return number < 0 ? (-number << 1) + 1 : (number << 1) + 0 +} + +export function base64VLQEncode(numbers: number[]) { + let encoded = '' + + for (const number of numbers) { + let vlq = toVLQSigned(number) + let digit: number + + do { + digit = vlq & VLQ_BASE_MASK + vlq >>>= VLQ_BASE_SHIFT + if (vlq > 0) { + // There are still more digits in this value, so we must make sure the + // continuation bit is marked. + digit |= VLQ_CONTINUATION_BIT + } + encoded += base64Encode(digit) + } while (vlq > 0) + } + + return encoded +} + const pkgPathCache = new Map() export function tryGetPkgPath(beginPath: string) { diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index c5c8b40..74df3ed 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -5,6 +5,7 @@ import { normalize, resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { + base64VLQEncode, ensureAbsolute, ensureArray, isNativeObj, @@ -106,4 +107,46 @@ describe('utils tests', () => { ) ).toBe(n('/project/src')) }) + + it('test: base64VLQEncode', () => { + // prettier-ignore + const snapshots = [ + '/P', '9P', '7P', '5P', '3P', '1P', 'zP', 'xP', 'vP', 'tP', 'rP', 'pP', 'nP', 'lP', 'jP', 'hP', + '/O', '9O', '7O', '5O', '3O', '1O', 'zO', 'xO', 'vO', 'tO', 'rO', 'pO', 'nO', 'lO', 'jO', 'hO', + '/N', '9N', '7N', '5N', '3N', '1N', 'zN', 'xN', 'vN', 'tN', 'rN', 'pN', 'nN', 'lN', 'jN', 'hN', + '/M', '9M', '7M', '5M', '3M', '1M', 'zM', 'xM', 'vM', 'tM', 'rM', 'pM', 'nM', 'lM', 'jM', 'hM', + '/L', '9L', '7L', '5L', '3L', '1L', 'zL', 'xL', 'vL', 'tL', 'rL', 'pL', 'nL', 'lL', 'jL', 'hL', + '/K', '9K', '7K', '5K', '3K', '1K', 'zK', 'xK', 'vK', 'tK', 'rK', 'pK', 'nK', 'lK', 'jK', 'hK', + '/J', '9J', '7J', '5J', '3J', '1J', 'zJ', 'xJ', 'vJ', 'tJ', 'rJ', 'pJ', 'nJ', 'lJ', 'jJ', 'hJ', + '/I', '9I', '7I', '5I', '3I', '1I', 'zI', 'xI', 'vI', 'tI', 'rI', 'pI', 'nI', 'lI', 'jI', 'hI', + '/H', '9H', '7H', '5H', '3H', '1H', 'zH', 'xH', 'vH', 'tH', 'rH', 'pH', 'nH', 'lH', 'jH', 'hH', + '/G', '9G', '7G', '5G', '3G', '1G', 'zG', 'xG', 'vG', 'tG', 'rG', 'pG', 'nG', 'lG', 'jG', 'hG', + '/F', '9F', '7F', '5F', '3F', '1F', 'zF', 'xF', 'vF', 'tF', 'rF', 'pF', 'nF', 'lF', 'jF', 'hF', + '/E', '9E', '7E', '5E', '3E', '1E', 'zE', 'xE', 'vE', 'tE', 'rE', 'pE', 'nE', 'lE', 'jE', 'hE', + '/D', '9D', '7D', '5D', '3D', '1D', 'zD', 'xD', 'vD', 'tD', 'rD', 'pD', 'nD', 'lD', 'jD', 'hD', + '/C', '9C', '7C', '5C', '3C', '1C', 'zC', 'xC', 'vC', 'tC', 'rC', 'pC', 'nC', 'lC', 'jC', 'hC', + '/B', '9B', '7B', '5B', '3B', '1B', 'zB', 'xB', 'vB', 'tB', 'rB', 'pB', 'nB', 'lB', 'jB', 'hB', + 'f', 'd', 'b', 'Z', 'X', 'V', 'T', 'R', 'P', 'N', 'L', 'J', 'H', 'F', 'D', 'A', 'C', 'E', 'G', + 'I', 'K', 'M', 'O', 'Q', 'S', 'U', 'W', 'Y', 'a', 'c', 'e', 'gB', 'iB', 'kB', 'mB', 'oB', 'qB', + 'sB', 'uB', 'wB', 'yB', '0B', '2B', '4B', '6B', '8B', '+B', 'gC', 'iC', 'kC', 'mC', 'oC', 'qC', + 'sC', 'uC', 'wC', 'yC', '0C', '2C', '4C', '6C', '8C', '+C', 'gD', 'iD', 'kD', 'mD', 'oD', 'qD', + 'sD', 'uD', 'wD', 'yD', '0D', '2D', '4D', '6D', '8D', '+D', 'gE', 'iE', 'kE', 'mE', 'oE', 'qE', + 'sE', 'uE', 'wE', 'yE', '0E', '2E', '4E', '6E', '8E', '+E', 'gF', 'iF', 'kF', 'mF', 'oF', 'qF', + 'sF', 'uF', 'wF', 'yF', '0F', '2F', '4F', '6F', '8F', '+F', 'gG', 'iG', 'kG', 'mG', 'oG', 'qG', + 'sG', 'uG', 'wG', 'yG', '0G', '2G', '4G', '6G', '8G', '+G', 'gH', 'iH', 'kH', 'mH', 'oH', 'qH', + 'sH', 'uH', 'wH', 'yH', '0H', '2H', '4H', '6H', '8H', '+H', 'gI', 'iI', 'kI', 'mI', 'oI', 'qI', + 'sI', 'uI', 'wI', 'yI', '0I', '2I', '4I', '6I', '8I', '+I', 'gJ', 'iJ', 'kJ', 'mJ', 'oJ', 'qJ', + 'sJ', 'uJ', 'wJ', 'yJ', '0J', '2J', '4J', '6J', '8J', '+J', 'gK', 'iK', 'kK', 'mK', 'oK', 'qK', + 'sK', 'uK', 'wK', 'yK', '0K', '2K', '4K', '6K', '8K', '+K', 'gL', 'iL', 'kL', 'mL', 'oL', 'qL', + 'sL', 'uL', 'wL', 'yL', '0L', '2L', '4L', '6L', '8L', '+L', 'gM', 'iM', 'kM', 'mM', 'oM', 'qM', + 'sM', 'uM', 'wM', 'yM', '0M', '2M', '4M', '6M', '8M', '+M', 'gN', 'iN', 'kN', 'mN', 'oN', 'qN', + 'sN', 'uN', 'wN', 'yN', '0N', '2N', '4N', '6N', '8N', '+N', 'gO', 'iO', 'kO', 'mO', 'oO', 'qO', + 'sO', 'uO', 'wO', 'yO', '0O', '2O', '4O', '6O', '8O', '+O', 'gP', 'iP', 'kP', 'mP', 'oP', 'qP', + 'sP', 'uP', 'wP', 'yP', '0P', '2P', '4P', '6P', '8P', '+P' + ] + + for (let i = 0, len = snapshots.length; i < len; ++i) { + expect(base64VLQEncode([i - 255])).toEqual(snapshots[i]) + } + }) })