Repository navigation
feat: add CSS source map support #6969
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
cb58cae
7dfb93b
918eaa9
bdbb38b
84705b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| import { SourceMapGenerator } from '@jridgewell/source-map'; | ||
| import { hasError, isFunction, isString } from '@utils'; | ||
| import sourceMapMerge from 'merge-source-map'; | ||
|
|
||
| import type * as d from '../../declarations'; | ||
| import { CssNode, CssNodeType } from '../style/css-parser/css-parse-declarations'; | ||
| import { parseCss } from '../style/css-parser/parse-css'; | ||
| import { serializeCss } from '../style/css-parser/serialize-css'; | ||
|
|
@@ -11,7 +14,13 @@ import { serializeCss } from '../style/css-parser/serialize-css'; | |
| * @param input An object containing the CSS string and an optional resolveUrl function to handle URL resolution. | ||
| * @returns A promise that resolves to the minified CSS string. | ||
| */ | ||
| export const minifyCss = async (input: { css: string; resolveUrl?: (url: string) => Promise<string> | string }) => { | ||
| export const minifyCss = async (input: { | ||
| css: string; | ||
| filePath?: string; | ||
| map?: d.SourceMap | string; | ||
| sourceMap?: boolean; | ||
| resolveUrl?: (url: string) => Promise<string> | string; | ||
| }): Promise<string | { output: string; map: d.SourceMap }> => { | ||
| const parseResults = parseCss(input.css); | ||
|
|
||
| if (hasError(parseResults.diagnostics)) { | ||
|
|
@@ -21,7 +30,62 @@ export const minifyCss = async (input: { css: string; resolveUrl?: (url: string) | |
| await resolveStylesheetUrl(parseResults.stylesheet.rules, input.resolveUrl, new Map()); | ||
| } | ||
|
|
||
| return serializeCss(parseResults.stylesheet, {}); | ||
| const output = serializeCss(parseResults.stylesheet, {}); | ||
| if (!input.sourceMap) { | ||
| return output; | ||
| } | ||
|
|
||
| const map = createCssSourceMap(input.css, output, input.filePath || getSourceMapFile(input.map)); | ||
| const previousMap = parseSourceMap(input.map); | ||
| return { | ||
| output, | ||
| map: previousMap ? sourceMapMerge(previousMap, map) : map, | ||
| }; | ||
| }; | ||
|
|
||
| const parseSourceMap = (map?: d.SourceMap | string) => { | ||
| if (typeof map === 'string') { | ||
| try { | ||
| return JSON.parse(map) as d.SourceMap; | ||
| } catch (e) { | ||
| return undefined; | ||
| } | ||
| } | ||
| return map; | ||
| }; | ||
|
|
||
| const getSourceMapFile = (map?: d.SourceMap | string) => parseSourceMap(map)?.file; | ||
|
|
||
| export const createCssSourceMap = (input: string, output: string, sourceFile = 'input.css'): d.SourceMap => { | ||
| const generator = new SourceMapGenerator({ file: sourceFile }); | ||
| generator.setSourceContent(sourceFile, input); | ||
|
|
||
| const tokenRe = /[^\s{};,:]+|[{};,:]/g; | ||
| let match: RegExpExecArray | null; | ||
| let searchFrom = 0; | ||
| while ((match = tokenRe.exec(output))) { | ||
| const token = match[0]; | ||
| const inputIndex = input.indexOf(token, searchFrom); | ||
| const originalIndex = inputIndex === -1 ? Math.min(searchFrom, input.length - 1) : inputIndex; | ||
| searchFrom = Math.max(searchFrom, originalIndex + token.length); | ||
| const original = getLineAndColumn(input, originalIndex); | ||
| const generated = getLineAndColumn(output, match.index); | ||
| generator.addMapping({ | ||
| generated, | ||
| original, | ||
| source: sourceFile, | ||
| name: '', | ||
| }); | ||
| } | ||
|
|
||
| return generator.toJSON() as d.SourceMap; | ||
| }; | ||
|
|
||
| const getLineAndColumn = (text: string, index: number) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. calling this repetitive can be quite expensive, we could consider a binary search instead, maybe something into this direction: https://github.com/cenfun/monocart-locator/blob/c227b9574146ee0b6a6ee7355df2ac747b91b979/lib/line-parser.js#L72
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Source-map line/column lookup now precomputes line starts and uses binary search instead of rescanning the prefix for every token. |
||
| const before = text.slice(0, Math.max(0, index)); | ||
| const line = (before.match(/\n/g) || []).length + 1; | ||
| const lastNewline = before.lastIndexOf('\n'); | ||
| return { line, column: index - lastNewline - 1 }; | ||
| }; | ||
|
|
||
| const resolveStylesheetUrl = async ( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to check if sourceMap.css is set as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the legacy TranspileOptions path, whose sourceMap option is boolean or inline; it does not use the validated build sourceMap.css object. sourceMapCss is therefore derived from compileOpts.sourceMap !== false here.