Skip to content

Commit 6f08506

Browse files
committed
feat: support inline source map
1 parent d59c14f commit 6f08506

File tree

5 files changed

+161
-93
lines changed

5 files changed

+161
-93
lines changed

src/getCodeAndMap.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as vscode from 'vscode'
2+
import { getSourceMapUrl } from './getSourceMapUrl'
3+
4+
export async function getCodeAndMap(): Promise<{ code: string; map: string } | undefined> {
5+
const editor = vscode.window.activeTextEditor
6+
if (!editor)
7+
return
8+
9+
const document = editor.document
10+
11+
const code = editor.selection.isEmpty ? document.getText() : editor.selections.map(document.getText).join('\n')
12+
if (!code)
13+
return
14+
15+
const file = document.fileName
16+
const dir = file.replace(/\/[^\/]+$/, '')
17+
const readTextFile = async (relativePath: string) => {
18+
try {
19+
const uri = vscode.Uri.file(`${dir}/${relativePath}`)
20+
const buf = await vscode.workspace.fs.readFile(uri)
21+
return new TextDecoder('utf-8').decode(buf)
22+
}
23+
catch (err) {
24+
console.warn('Error reading file', err)
25+
}
26+
}
27+
28+
const mapUrl = getSourceMapUrl(code)
29+
if (mapUrl) {
30+
if (mapUrl.startsWith('data:')) {
31+
const [type, data] = mapUrl.split(',')
32+
const map = new TextDecoder('utf-8').decode(Buffer.from(data, type.includes('base64') ? 'base64' : 'utf-8'))
33+
if (map)
34+
return { code, map }
35+
}
36+
else if (/^https?:/.test(mapUrl)) {
37+
// TODO: fetch map from url
38+
}
39+
else if (mapUrl.startsWith('/')) {
40+
// TODO: read map from absolute path?
41+
}
42+
else if (!document.isUntitled) {
43+
const map = await readTextFile(mapUrl)
44+
if (map)
45+
return { code, map }
46+
}
47+
}
48+
49+
if (document.isUntitled)
50+
return
51+
52+
const fileName = file.split('/').pop()
53+
if (fileName) {
54+
const filelist = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dir))
55+
const mapFiles = filelist.map(([name]) => name).filter(f => f.endsWith('.map'))
56+
const mapFile
57+
= mapFiles.find(f => f === `${fileName}.map`)
58+
|| mapFiles.find(f => fileName.startsWith(f.split('.')[0]))
59+
if (mapFile) {
60+
const map = await readTextFile(mapFile)
61+
if (map)
62+
return { code, map }
63+
}
64+
}
65+
}

src/getHtmlForWebview.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as vscode from 'vscode'
2+
import { getNonce } from './getNonce'
3+
4+
export function getHtmlForWebview(webview: vscode.Webview, extensionUri: vscode.Uri) {
5+
// Get the local path to main script run in the webview, then convert it to a uri we can use in the webview.
6+
const mainScriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'res', 'main.js'))
7+
const codeScriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'res', 'code.js'))
8+
9+
// Do the same for the stylesheet.
10+
const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'res', 'style.css'))
11+
12+
// Use a nonce to only allow a specific script to be run.
13+
const nonce = getNonce()
14+
15+
return `<!DOCTYPE html>
16+
<html lang="en">
17+
<head>
18+
<meta charset="utf-8" />
19+
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';"> -->
20+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
21+
<link href="${styleUri}" rel="stylesheet">
22+
</head>
23+
<body>
24+
<div id="toolbar">
25+
<section>
26+
<h2>Original code</h2>
27+
<div id="fileListParent"><select id="fileList"></select></div>
28+
</section>
29+
<section>
30+
<h2>Generated code</h2>
31+
</section>
32+
</div>
33+
<div id="statusBar">
34+
<section>
35+
<div id="originalStatus"></div>
36+
</section>
37+
<section>
38+
<div id="generatedStatus"></div>
39+
</section>
40+
</div>
41+
<script nonce="${nonce}" src="${mainScriptUri}"></script>
42+
<script nonce="${nonce}" src="${codeScriptUri}"></script>
43+
</body>
44+
</html>`
45+
}

src/getNonce.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function getNonce() {
2+
let text = ''
3+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
4+
for (let i = 0; i < 32; i++)
5+
text += possible.charAt(Math.floor(Math.random() * possible.length))
6+
7+
return text
8+
}
9+
10+
if (import.meta.vitest) {
11+
const { it, expect } = import.meta.vitest
12+
13+
it('get nonce', () => {
14+
expect(getNonce()).toBeTypeOf('string')
15+
})
16+
}

src/getSourceMapUrl.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export function getSourceMapUrl(code: string): string | undefined {
2+
/** Check for both `//` and `/*` comments */
3+
const match
4+
= /\/(\/)[#@] *sourceMappingURL=([^\s]+)/.exec(code)
5+
|| /\/(\*)[#@] *sourceMappingURL=((?:[^\s*]|\*[^/])+)(?:[^*]|\*[^/])*\*\//.exec(code)
6+
7+
return match?.[2]
8+
}
9+
10+
if (import.meta.vitest) {
11+
const { it, expect } = import.meta.vitest
12+
13+
it.each([
14+
'//# sourceMappingURL=data:application/json,{}',
15+
'//@ sourceMappingURL=data:application/json,{}',
16+
'/*# sourceMappingURL=data:application/json,{} */',
17+
'/*@ sourceMappingURL=data:application/json,{} */',
18+
])('get source map data uri', (code) => {
19+
expect(getSourceMapUrl(code)).toBe('data:application/json,{}')
20+
})
21+
22+
it('get source map link', () => {
23+
expect(getSourceMapUrl('//# sourceMappingURL=http://example.com/index.js.map')).toBe('http://example.com/index.js.map')
24+
})
25+
26+
it('get source map file name', () => {
27+
expect(getSourceMapUrl('//# sourceMappingURL=index.js.map')).toBe('index.js.map')
28+
})
29+
}

src/index.ts

Lines changed: 6 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as vscode from 'vscode'
2+
import { getCodeAndMap } from './getCodeAndMap'
3+
import { getHtmlForWebview } from './getHtmlForWebview'
24

35
export function activate(context: vscode.ExtensionContext) {
46
// Track currently webview panel
@@ -56,7 +58,10 @@ function getViewColumn() {
5658
async function updatePanel(panel: vscode.WebviewPanel) {
5759
if (!panel)
5860
return
59-
const data = await getCodeAndMap()
61+
const data = await getCodeAndMap().catch((err) => {
62+
console.warn('Get code and map error:', err)
63+
return undefined
64+
})
6065
if (!data)
6166
return
6267
panel.webview.postMessage({
@@ -65,95 +70,3 @@ async function updatePanel(panel: vscode.WebviewPanel) {
6570
})
6671
}
6772

68-
async function getCodeAndMap() {
69-
const editor = vscode.window.activeTextEditor
70-
if (!editor)
71-
return
72-
73-
const document = editor.document
74-
75-
const file = document.isUntitled ? 'Untitled Document' : document.fileName
76-
const dir = file.replace(/\/[^\/]+$/, '')
77-
const fileName = file.split('/').pop()
78-
if (!fileName)
79-
return
80-
81-
const fileMetas = await Promise.resolve(vscode.workspace.fs.readDirectory(vscode.Uri.file(dir))).catch(() => [])
82-
let mapFileName = fileMetas.find(([name]) => name === `${fileName}.map`)?.[0]
83-
mapFileName ??= fileMetas.find(([name]) => name.startsWith(fileName?.split('.')[0]) && name.endsWith('.map'))?.[0]
84-
85-
const selectedCode = document.getText(editor.selection)
86-
const getFullCode = async () => Promise.resolve(vscode.workspace.fs.readFile(vscode.Uri.file(file)).then(buffer => new TextDecoder('utf-8').decode(buffer))).catch(() => document.getText())
87-
const code = selectedCode || await getFullCode()
88-
if (!mapFileName) {
89-
const lastLine = (selectedCode ? await getFullCode() : code).split('\n').pop() ?? ''
90-
if (!lastLine.startsWith('//# sourceMappingURL=')) {
91-
vscode.window.setStatusBarMessage('Source map not found!', 5000)
92-
return
93-
}
94-
const map = new TextDecoder('utf-8').decode(Buffer.from(lastLine.split(',').pop() ?? '', 'base64'))
95-
if (!map) {
96-
vscode.window.setStatusBarMessage('Source map not found!', 5000)
97-
return
98-
}
99-
return { code, map }
100-
}
101-
102-
const mapFile = `${dir}/${mapFileName}`
103-
104-
const map = await vscode.workspace.fs.readFile(vscode.Uri.file(mapFile)).then(buffer => new TextDecoder('utf-8').decode(buffer))
105-
106-
return { code, map }
107-
}
108-
109-
function getHtmlForWebview(webview: vscode.Webview, extensionUri: vscode.Uri) {
110-
// Get the local path to main script run in the webview, then convert it to a uri we can use in the webview.
111-
const mainScriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'res', 'main.js'))
112-
const codeScriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'res', 'code.js'))
113-
114-
// Do the same for the stylesheet.
115-
const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'res', 'style.css'))
116-
117-
// Use a nonce to only allow a specific script to be run.
118-
const nonce = getNonce()
119-
120-
return `<!DOCTYPE html>
121-
<html lang="en">
122-
<head>
123-
<meta charset="utf-8" />
124-
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';"> -->
125-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
126-
<link href="${styleUri}" rel="stylesheet">
127-
</head>
128-
<body>
129-
<div id="toolbar">
130-
<section>
131-
<h2>Original code</h2>
132-
<div id="fileListParent"><select id="fileList"></select></div>
133-
</section>
134-
<section>
135-
<h2>Generated code</h2>
136-
</section>
137-
</div>
138-
<div id="statusBar">
139-
<section>
140-
<div id="originalStatus"></div>
141-
</section>
142-
<section>
143-
<div id="generatedStatus"></div>
144-
</section>
145-
</div>
146-
<script nonce="${nonce}" src="${mainScriptUri}"></script>
147-
<script nonce="${nonce}" src="${codeScriptUri}"></script>
148-
</body>
149-
</html>`
150-
}
151-
152-
function getNonce() {
153-
let text = ''
154-
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
155-
for (let i = 0; i < 32; i++)
156-
text += possible.charAt(Math.floor(Math.random() * possible.length))
157-
158-
return text
159-
}

0 commit comments

Comments
 (0)