Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(worker): Add sourcemap support for worker bundles #5417

Merged
merged 13 commits into from Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,81 @@
import fs from 'fs'
import path from 'path'
import { untilUpdated, isBuild, testDir } from '../../testUtils'
import { Page } from 'playwright-chromium'

// Workaround so that testing serve does not emit
// "Your test suite must contain at least one test"
test('true', () => {
expect(true).toBe(true)
})

if (isBuild) {
// assert correct files
test('hidden sourcemap generation for web workers', async () => {
const assetsDir = path.resolve(testDir, 'dist/assets')
const files = fs.readdirSync(assetsDir)
// should have 2 worker chunk
expect(files.length).toBe(6)
const index = files.find((f) => /^index\.\w+\.js$/.test(f))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const indexSourcemap = getSourceMapUrl(content)
const worker = files.find((f) => /^my-worker\.\w+\.js$/.test(f))
const workerContent = fs.readFileSync(
path.resolve(assetsDir, worker),
'utf-8'
)
const workerSourcemap = getSourceMapUrl(workerContent)
const sharedWorker = files.find((f) =>
/^my-shared-worker\.\w+\.js$/.test(f)
)
const sharedWorkerContent = fs.readFileSync(
path.resolve(assetsDir, sharedWorker),
'utf-8'
)
const sharedWorkerSourcemap = getSourceMapUrl(sharedWorkerContent)

expect(files).toContainEqual(expect.stringMatching(/^index\.\w+\.js\.map$/))
expect(files).toContainEqual(
expect.stringMatching(/^my-worker\.\w+\.js\.map$/)
)
expect(files).toContainEqual(
expect.stringMatching(/^my-shared-worker\.\w+\.js\.map$/)
)

// sourcemap should exist and have a data URL
expect(indexSourcemap).toBe(null)
expect(workerSourcemap).toBe(null)
expect(sharedWorkerSourcemap).toBe(null)

// worker should have all imports resolved and no exports
expect(workerContent).not.toMatch(`import`)
expect(workerContent).not.toMatch(`export`)

// shared worker should have all imports resolved and no exports
expect(sharedWorkerContent).not.toMatch(`import`)
expect(sharedWorkerContent).not.toMatch(`export`)

// chunk
expect(content).toMatch(`new Worker("/assets`)
expect(content).toMatch(`new SharedWorker("/assets`)
// inlined
expect(content).toMatch(`(window.URL||window.webkitURL).createObjectURL`)
expect(content).toMatch(`window.Blob`)
})
} else {
// Workaround so that testing serve does not emit
// "Your test suite must contain at least one test"
test('true', () => {
expect(true).toBe(true)
})
}

function getSourceMapUrl(code: string): string {
const regex = /\/\/[#@]\s(?:source(?:Mapping)?URL)=\s*(\S+)/g
const results = regex.exec(code)

if (results && results.length >= 2) {
return results[1]
}
return null
}
57 changes: 57 additions & 0 deletions packages/playground/sourcemap-hidden/index.html
@@ -0,0 +1,57 @@
<div>Expected values: <span class="mode-true"></span></div>
<button class="ping">Ping</button>
<div>
Response from worker: <span class="pong"></span><span class="mode"></span>
</div>

<button class="ping-inline">Ping Inline Worker</button>
<div>Response from inline worker: <span class="pong-inline"></span></div>

<button class="tick-shared">Tick Shared Worker</button>
<div>
Tick from shared worker, it syncs between pages:
<span class="tick-count">0</span>
</div>

<script type="module">
import Worker from './my-worker?worker'
import InlineWorker from './my-worker?worker&inline'
import SharedWorker from './my-shared-worker?sharedworker&name=shared'
import { mode } from './workerImport'

document.querySelector('.mode-true').textContent = mode

const worker = new Worker()
worker.addEventListener('message', (e) => {
text('.pong', e.data.msg)
text('.mode', e.data.mode)
})

document.querySelector('.ping').addEventListener('click', () => {
worker.postMessage('ping')
})

const inlineWorker = new InlineWorker()
inlineWorker.addEventListener('message', (e) => {
text('.pong-inline', e.data.msg)
})

document.querySelector('.ping-inline').addEventListener('click', () => {
inlineWorker.postMessage('ping')
})

const sharedWorker = new SharedWorker()
document.querySelector('.tick-shared').addEventListener('click', () => {
sharedWorker.port.postMessage('tick')
})

sharedWorker.port.addEventListener('message', (event) => {
text('.tick-count', event.data)
})

sharedWorker.port.start()

function text(el, text) {
document.querySelector(el).textContent = text
}
</script>
16 changes: 16 additions & 0 deletions packages/playground/sourcemap-hidden/my-shared-worker.ts
@@ -0,0 +1,16 @@
let count = 0
const ports = new Set()

onconnect = (event) => {
const port = event.ports[0]
ports.add(port)
port.postMessage(count)
port.onmessage = (message) => {
if (message.data === 'tick') {
count++
ports.forEach((p) => {
p.postMessage(count)
})
}
}
}
7 changes: 7 additions & 0 deletions packages/playground/sourcemap-hidden/my-worker.ts
@@ -0,0 +1,7 @@
import { msg, mode } from './workerImport'

self.onmessage = (e) => {
if (e.data === 'ping') {
self.postMessage({ msg, mode })
}
}
11 changes: 11 additions & 0 deletions packages/playground/sourcemap-hidden/package.json
@@ -0,0 +1,11 @@
{
"name": "test-sourcemap-hidden",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"debug": "node --inspect-brk ../../vite/bin/vite",
"serve": "vite preview"
}
}
7 changes: 7 additions & 0 deletions packages/playground/sourcemap-hidden/vite.config.ts
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'

export default defineConfig({
build: {
sourcemap: 'hidden'
}
})
2 changes: 2 additions & 0 deletions packages/playground/sourcemap-hidden/workerImport.js
@@ -0,0 +1,2 @@
export const msg = 'pong'
export const mode = process.env.NODE_ENV
@@ -0,0 +1,65 @@
import fs from 'fs'
import path from 'path'
import { untilUpdated, isBuild, testDir } from '../../testUtils'
import { Page } from 'playwright-chromium'

if (isBuild) {
// assert correct files
test('inlined sourcemap generation for web workers', async () => {
const assetsDir = path.resolve(testDir, 'dist/assets')
const files = fs.readdirSync(assetsDir)
// should have 2 worker chunk
expect(files.length).toBe(3)
const index = files.find((f) => f.includes('index'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const indexSourcemap = getSourceMapUrl(content)
const worker = files.find((f) => f.includes('my-worker'))
const workerContent = fs.readFileSync(
path.resolve(assetsDir, worker),
'utf-8'
)
const workerSourcemap = getSourceMapUrl(workerContent)
const sharedWorker = files.find((f) => f.includes('my-shared-worker'))
const sharedWorkerContent = fs.readFileSync(
path.resolve(assetsDir, sharedWorker),
'utf-8'
)
const sharedWorkerSourcemap = getSourceMapUrl(sharedWorkerContent)

// sourcemap should exist and have a data URL
expect(indexSourcemap).toMatch(/^data:/)
expect(workerSourcemap).toMatch(/^data:/)
expect(sharedWorkerSourcemap).toMatch(/^data:/)

// worker should have all imports resolved and no exports
expect(workerContent).not.toMatch(`import`)
expect(workerContent).not.toMatch(`export`)

// shared worker should have all imports resolved and no exports
expect(sharedWorkerContent).not.toMatch(`import`)
expect(sharedWorkerContent).not.toMatch(`export`)

// chunk
expect(content).toMatch(`new Worker("/assets`)
expect(content).toMatch(`new SharedWorker("/assets`)
// inlined
expect(content).toMatch(`(window.URL||window.webkitURL).createObjectURL`)
expect(content).toMatch(`window.Blob`)
})
} else {
// Workaround so that testing serve does not emit
// "Your test suite must contain at least one test"
test('true', () => {
expect(true).toBe(true)
})
}

function getSourceMapUrl(code: string): string {
const regex = /\/\/[#@]\s(?:source(?:Mapping)?URL)=\s*(\S+)/g
const results = regex.exec(code)

if (results && results.length >= 2) {
return results[1]
}
return null
}
57 changes: 57 additions & 0 deletions packages/playground/sourcemap-inline/index.html
@@ -0,0 +1,57 @@
<div>Expected values: <span class="mode-true"></span></div>
<button class="ping">Ping</button>
<div>
Response from worker: <span class="pong"></span><span class="mode"></span>
</div>

<button class="ping-inline">Ping Inline Worker</button>
<div>Response from inline worker: <span class="pong-inline"></span></div>

<button class="tick-shared">Tick Shared Worker</button>
<div>
Tick from shared worker, it syncs between pages:
<span class="tick-count">0</span>
</div>

<script type="module">
import Worker from './my-worker?worker'
import InlineWorker from './my-worker?worker&inline'
import SharedWorker from './my-shared-worker?sharedworker&name=shared'
import { mode } from './workerImport'

document.querySelector('.mode-true').textContent = mode

const worker = new Worker()
worker.addEventListener('message', (e) => {
text('.pong', e.data.msg)
text('.mode', e.data.mode)
})

document.querySelector('.ping').addEventListener('click', () => {
worker.postMessage('ping')
})

const inlineWorker = new InlineWorker()
inlineWorker.addEventListener('message', (e) => {
text('.pong-inline', e.data.msg)
})

document.querySelector('.ping-inline').addEventListener('click', () => {
inlineWorker.postMessage('ping')
})

const sharedWorker = new SharedWorker()
document.querySelector('.tick-shared').addEventListener('click', () => {
sharedWorker.port.postMessage('tick')
})

sharedWorker.port.addEventListener('message', (event) => {
text('.tick-count', event.data)
})

sharedWorker.port.start()

function text(el, text) {
document.querySelector(el).textContent = text
}
</script>
16 changes: 16 additions & 0 deletions packages/playground/sourcemap-inline/my-shared-worker.ts
@@ -0,0 +1,16 @@
let count = 0
const ports = new Set()

onconnect = (event) => {
const port = event.ports[0]
ports.add(port)
port.postMessage(count)
port.onmessage = (message) => {
if (message.data === 'tick') {
count++
ports.forEach((p) => {
p.postMessage(count)
})
}
}
}
7 changes: 7 additions & 0 deletions packages/playground/sourcemap-inline/my-worker.ts
@@ -0,0 +1,7 @@
import { msg, mode } from './workerImport'

self.onmessage = (e) => {
if (e.data === 'ping') {
self.postMessage({ msg, mode })
}
}
11 changes: 11 additions & 0 deletions packages/playground/sourcemap-inline/package.json
@@ -0,0 +1,11 @@
{
"name": "test-sourcemap-inline",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"debug": "node --inspect-brk ../../vite/bin/vite",
"serve": "vite preview"
}
}
7 changes: 7 additions & 0 deletions packages/playground/sourcemap-inline/vite.config.ts
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'

export default defineConfig({
build: {
sourcemap: 'inline'
}
})
2 changes: 2 additions & 0 deletions packages/playground/sourcemap-inline/workerImport.js
@@ -0,0 +1,2 @@
export const msg = 'pong'
export const mode = process.env.NODE_ENV