Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "sourcemap_tools_devcontainer",
"image": "public.ecr.aws/docker/library/node:24",
"workspaceMount": "source=${localWorkspaceFolder},target=/sm,type=bind",
"workspaceFolder": "/sm",
"runArgs": ["--name", "sourcemap_tools_devcontainer", "--tmpfs", "/tmp:exec,mode=01777"],
"customizations": {
"vscode": {
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"],
"settings": {
"editor.rulers": [100],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"npm.packageManager": "npm",
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}
},
"postCreateCommand": "cd /sm && npm ci",
"remoteUser": "node"
}
59 changes: 59 additions & 0 deletions src/__tests__/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,65 @@ describe('source maps', () => {
// Should display the fallback with generated ID
expect(listItem).toHaveTextContent(/NO NAME \(Generated id:/)
})

test('decodes base64 data URL source map pasted into textarea', async () => {
render(<App />)
const user = userEvent.setup()

const sourcemapTextarea = screen.getByRole('textbox', { name: /source map/i })

const dataUrl = `data:application/json;base64,${btoa(regular.sourcemaps[0].content)}`

sourcemapTextarea.focus()
await user.paste(dataUrl)

const sourcemapList = await screen.findByRole('list', { name: /sourcemaps list/i })
expect(within(sourcemapList).getByRole('listitem')).toHaveTextContent('index-d803759c.js')
})

test('decodes base64 data URL with charset parameter', async () => {
render(<App />)
const user = userEvent.setup()

const sourcemapTextarea = screen.getByRole('textbox', { name: /source map/i })

const dataUrl = `data:application/json;charset=utf-8;base64,${btoa(regular.sourcemaps[0].content)}`

sourcemapTextarea.focus()
await user.paste(dataUrl)

const sourcemapList = await screen.findByRole('list', { name: /sourcemaps list/i })
expect(within(sourcemapList).getByRole('listitem')).toHaveTextContent('index-d803759c.js')
})

test('shows warning for malformed base64 data URL', async () => {
render(<App />)
const user = userEvent.setup()

const sourcemapTextarea = screen.getByRole('textbox', { name: /source map/i })

sourcemapTextarea.focus()
await user.paste('data:application/json;base64,!!!not-valid-base64!!!')

const warning = await screen.findByText(/provided text is not a source map/i)
expect(warning).toBeInTheDocument()

// The original (undecoded) value should remain in the textarea so the user can fix it.
expect(sourcemapTextarea).toHaveValue('data:application/json;base64,!!!not-valid-base64!!!')
})

test('treats plain source map text as non-base64', async () => {
render(<App />)
const user = userEvent.setup()

const sourcemapTextarea = screen.getByRole('textbox', { name: /source map/i })

sourcemapTextarea.focus()
await user.paste(regular.sourcemaps[1].content)

const sourcemapList = await screen.findByRole('list', { name: /sourcemaps list/i })
expect(within(sourcemapList).getByRole('listitem')).toHaveTextContent('vendor-221d27ba.js')
})
})

describe('theme', () => {
Expand Down
17 changes: 16 additions & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { ThemeToggle } from './theme-toggle.tsx'
import { useSourcemapsStore } from './use-sourcemaps-store.ts'
import { setTheme, useTheme } from './use-theme.ts'

const base64PrefixRegex =
/^(?:\/\/# sourceMappingURL=)?data:application\/json;(?:charset=[^;]+;)?base64,/

export default function App() {
const [stackTraceInputValue, setStackTraceInputValue] = useState('')
const [sourceMapInputValue, setSourceMapInputValue] = useState('')
Expand Down Expand Up @@ -43,7 +46,19 @@ export default function App() {
}

async function handleSourceMapTextAreaChange(event: ChangeEvent<HTMLTextAreaElement>) {
const text = event.target.value
let text = event.target.value

if (base64PrefixRegex.test(text)) {
try {
const base64Content = text.replace(base64PrefixRegex, '')
const bytes = Uint8Array.from(atob(base64Content), c => c.charCodeAt(0))
text = new TextDecoder().decode(bytes)
} catch {
setIsSourceMapInputError(true)
setSourceMapInputValue(event.target.value)
return
}
}

setSourceMapInputValue(text)

Expand Down
Loading