Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions src/compiler/build/build-hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const generateHmr = (config: d.Config, compilerCtx: d.CompilerCtx, buildC
styleId: getScopeId(s.styleTag, s.styleMode),
styleTag: s.styleTag,
styleText: s.styleText,
styleSourceMap: s.styleSourceMap,
} as d.HmrStyleUpdate;
}),
(s) => s.styleId,
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/bundle/app-data-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const appDataPlugin = (
});
buildCtx.diagnostics.push(...loadTypeScriptDiagnostics(results.diagnostics));

if (config.sourceMap) {
if (config.sourceMapJs) {
// generate the sourcemap for global script
const codeMs = new MagicString(code);
const codeMap = codeMs.generateMap({
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/bundle/bundle-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export const getRollupOptions = (
resolveIdWithTypeScript(config, compilerCtx),
rollupCommonjsPlugin({
include: /node_modules/,
sourceMap: config.sourceMap,
sourceMap: config.sourceMapJs,
transformMixedEsModules: false,
...config.commonjs,
}),
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/bundle/ext-transforms-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ export const extTransformsPlugin = (
addTagTransformers: !!buildCtx.config.extras.additionalTagTransformers,
encapsulation: data.encapsulation,
mode: data.mode,
sourceMap: config.sourceMap,
sourceMap: config.sourceMapCss,
map: pluginTransforms.map,
minify: config.minifyCss,
autoprefixer: config.autoprefixCss,
docs: config.buildDocs,
Expand Down Expand Up @@ -199,6 +200,7 @@ export const extTransformsPlugin = (
styleTag: data.tag,
styleMode: data.mode,
styleText,
styleSourceMap: cssTransformResults.styleSourceMap,
});
}

Expand Down
33 changes: 33 additions & 0 deletions src/compiler/bundle/test/ext-transforms-plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,38 @@ describe('extTransformsPlugin', () => {

expect(css).toBe(':host { text: pink; }');
});

it('should pass the CSS source-map setting and plugin map to the CSS transform', async () => {
const { plugin, config, transformCssToEsmSpy } = setup();
const map = {
version: 3,
file: '/some/stubbed/path/foo.css',
names: [] as string[],
mappings: 'AAAA',
sources: ['/some/stubbed/path/foo.scss'],
sourcesContent: [':host { text: pink; }'],
};

config.sourceMap = { css: true, js: false } as any;
(config as any).sourceMapCss = true;
config.plugins = [
{
transform: () => ({
code: ':host { text: pink; }',
map: JSON.stringify(map),
}),
},
];

// @ts-ignore the Rollup plugins expect to be called in a Rollup context
await plugin.transform('asdf', '/some/stubbed/path/foo.css?tag=my-component');

expect(transformCssToEsmSpy).toHaveBeenCalledWith(
expect.objectContaining({
sourceMap: true,
map,
}),
);
});
});
});
39 changes: 33 additions & 6 deletions src/compiler/config/test/validate-config-sourcemap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('stencil config - sourceMap option', () => {
const { config: validatedConfig } = validateConfig(userConfig, {});
expect(validatedConfig.sourceMap).toBe(true);

mockTsConfigParser(validatedConfig.sourceMap);
mockTsConfigParser(validatedConfig.sourceMapJs);
const testConfig = getLoadConfigForTests({ config: userConfig });
const loadConfigResults = await loadConfig(testConfig);

Expand All @@ -73,7 +73,7 @@ describe('stencil config - sourceMap option', () => {
const { config: validatedConfig } = validateConfig(userConfig, {});
expect(validatedConfig.sourceMap).toBe(false);

mockTsConfigParser(validatedConfig.sourceMap);
mockTsConfigParser(validatedConfig.sourceMapJs);
const testConfig = getLoadConfigForTests({ config: userConfig });
const loadConfigResults = await loadConfig(testConfig);

Expand All @@ -87,7 +87,7 @@ describe('stencil config - sourceMap option', () => {
const { config: validatedConfig } = validateConfig(userConfig, {});
expect(validatedConfig.sourceMap).toBe(false);

mockTsConfigParser(validatedConfig.sourceMap);
mockTsConfigParser(validatedConfig.sourceMapJs);
const testConfig = getLoadConfigForTests({ config: userConfig });
const loadConfigResults = await loadConfig(testConfig);

Expand All @@ -101,7 +101,7 @@ describe('stencil config - sourceMap option', () => {
const { config: validatedConfig } = validateConfig(userConfig, {});
expect(validatedConfig.sourceMap).toBe(true);

mockTsConfigParser(validatedConfig.sourceMap);
mockTsConfigParser(validatedConfig.sourceMapJs);
const testConfig = getLoadConfigForTests({ config: userConfig });
const loadConfigResults = await loadConfig(testConfig);

Expand All @@ -115,7 +115,7 @@ describe('stencil config - sourceMap option', () => {
const { config: validatedConfig } = validateConfig(userConfig, {});
expect(validatedConfig.sourceMap).toBe(true);

mockTsConfigParser(validatedConfig.sourceMap);
mockTsConfigParser(validatedConfig.sourceMapJs);
const testConfig = getLoadConfigForTests({ config: userConfig });
const loadConfigResults = await loadConfig(testConfig);

Expand All @@ -129,12 +129,39 @@ describe('stencil config - sourceMap option', () => {
const { config: validatedConfig } = validateConfig(userConfig, {});
expect(validatedConfig.sourceMap).toBe(false);

mockTsConfigParser(validatedConfig.sourceMap);
mockTsConfigParser(validatedConfig.sourceMapJs);
const testConfig = getLoadConfigForTests({ config: userConfig });
const loadConfigResults = await loadConfig(testConfig);

const { sourceMap, inlineSources } = loadConfigResults.config.tsCompilerOptions;
expect(sourceMap).toBe(false);
expect(inlineSources).toBe(false);
});

it('enables only CSS source maps when configured with css', () => {
const userConfig: d.UnvalidatedConfig = { sourceMap: { css: true }, devMode: false };
const { config: validatedConfig } = validateConfig(userConfig, {});

expect(validatedConfig.sourceMap).toEqual({ css: true });
expect(validatedConfig.sourceMapCss).toBe(true);
expect(validatedConfig.sourceMapJs).toBe(false);
});

it('enables only JavaScript source maps when configured with js', () => {
const userConfig: d.UnvalidatedConfig = { sourceMap: { js: true }, devMode: false };
const { config: validatedConfig } = validateConfig(userConfig, {});

expect(validatedConfig.sourceMap).toEqual({ js: true });
expect(validatedConfig.sourceMapCss).toBe(false);
expect(validatedConfig.sourceMapJs).toBe(true);
});

it('keeps legacy boolean source maps enabled for JavaScript only', () => {
const userConfig: d.UnvalidatedConfig = { sourceMap: true, devMode: false };
const { config: validatedConfig } = validateConfig(userConfig, {});

expect(validatedConfig.sourceMap).toBe(true);
expect(validatedConfig.sourceMapCss).toBe(false);
expect(validatedConfig.sourceMapJs).toBe(true);
});
});
1 change: 1 addition & 0 deletions src/compiler/config/transpile-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export const getTranspileConfig = (input: TranspileOptions): TranspileConfig =>
file: compileOpts.file,
style: compileOpts.style as any,
styleImportData: compileOpts.styleImportData as any,
sourceMapCss: compileOpts.sourceMap !== false,

Copy link
Copy Markdown

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?

Copy link
Copy Markdown
Author

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.

target: compileOpts.target as any,
};

Expand Down
21 changes: 19 additions & 2 deletions src/compiler/config/validate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const validateConfig = (
}

const hashFileNames = config.hashFileNames ?? !devMode;
const sourceMap = validateSourceMap(config.sourceMap, devMode);

const validatedConfig: ValidatedConfig = {
devServer: {}, // assign `devServer` before spreading `config`, in the event 'devServer' is not a key on `config`
Expand All @@ -135,8 +136,7 @@ export const validateConfig = (
minifyJs: config.minifyJs ?? !devMode,
outputTargets: config.outputTargets ?? [],
rollupConfig: validateRollupConfig(config),
sourceMap:
config.sourceMap === true || (devMode && (config.sourceMap === 'dev' || typeof config.sourceMap === 'undefined')),
...sourceMap,
sys: config.sys ?? bootstrapConfig.sys ?? createNodeSys({ logger }),
testing: config.testing ?? {},
docs: validateDocs(config, logger),
Expand Down Expand Up @@ -299,3 +299,20 @@ export const validateConfig = (
diagnostics,
};
};

const validateSourceMap = (sourceMap: UnvalidatedConfig['sourceMap'], devMode: boolean) => {
if (sourceMap != null && typeof sourceMap === 'object') {
return {
sourceMap,
sourceMapCss: sourceMap.css === true,
sourceMapJs: sourceMap.js === true,
};
}

const enabled = sourceMap === true || (devMode && (sourceMap === 'dev' || sourceMap == null));
return {
sourceMap: enabled,
sourceMapCss: false,
sourceMapJs: enabled,
};
};
21 changes: 19 additions & 2 deletions src/compiler/optimize/autoprefixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ let cssProcessor: CssProcessor;
* @param opts an optional param with options for Autoprefixer
* @returns a Promise wrapping some prefixed CSS as well as diagnostics
*/
export const autoprefixCss = async (cssText: string, opts: boolean | null | d.AutoprefixerOptions) => {
export const autoprefixCss = async (
cssText: string,
opts: boolean | null | d.AutoprefixerOptions,
sourceMap?: d.SourceMap | string,
filePath?: string,
) => {
const output: d.OptimizeCssOutput = {
output: cssText,
diagnostics: [],
Expand All @@ -26,7 +31,16 @@ export const autoprefixCss = async (cssText: string, opts: boolean | null | d.Au
const autoprefixerOpts = opts != null && typeof opts === 'object' ? opts : DEFAULT_AUTOPREFIX_OPTIONS;

const processor = getProcessor(autoprefixerOpts);
const result = await processor.process(cssText, { map: null });
const result = await processor.process(cssText, {
from: filePath,
map: sourceMap
? {
annotation: false,
inline: false,
prev: sourceMap,
}
: null,
});

result.warnings().forEach((warning: any) => {
output.diagnostics.push({
Expand All @@ -39,6 +53,9 @@ export const autoprefixCss = async (cssText: string, opts: boolean | null | d.Au
});

output.output = result.css;
if (result.map) {
output.map = result.map.toJSON();
}
} catch (e: any) {
const diagnostic: d.Diagnostic = {
header: `Autoprefix CSS`,
Expand Down
68 changes: 66 additions & 2 deletions src/compiler/optimize/minify-css.ts
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';
Expand All @@ -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)) {
Expand All @@ -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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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 (
Expand Down
24 changes: 21 additions & 3 deletions src/compiler/optimize/optimize-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { hasError } from '@utils';

import { OptimizeCssInput, OptimizeCssOutput } from '../../declarations';
import { autoprefixCss } from './autoprefixer';
import { minifyCss } from './minify-css';
import { createCssSourceMap, minifyCss } from './minify-css';

/**
* Optimize a CSS file, optionally running an autoprefixer and a minifier
Expand All @@ -15,18 +15,36 @@ export const optimizeCss = async (inputOpts: OptimizeCssInput): Promise<Optimize
let result: OptimizeCssOutput = {
output: inputOpts.input,
diagnostics: [],
map: inputOpts.sourceMap ? inputOpts.map : undefined,
};
if (inputOpts.autoprefixer !== false && inputOpts.autoprefixer !== null) {
result = await autoprefixCss(inputOpts.input, inputOpts.autoprefixer ?? null);
result = await autoprefixCss(
inputOpts.input,
inputOpts.autoprefixer ?? null,
inputOpts.sourceMap ? inputOpts.map : undefined,
inputOpts.filePath,
);
if (hasError(result.diagnostics)) {
return result;
}
}
if (inputOpts.sourceMap && !result.map) {
result.map = createCssSourceMap(result.output, result.output, inputOpts.filePath || 'input.css');
}
if (inputOpts.minify !== false) {
result.output = await minifyCss({
const minifyResult = await minifyCss({
css: result.output,
filePath: inputOpts.filePath,
map: result.map,
sourceMap: inputOpts.sourceMap,
resolveUrl: inputOpts.resolveUrl,
});
if (typeof minifyResult === 'string') {
result.output = minifyResult;
} else {
result.output = minifyResult.output;
result.map = minifyResult.map;
}
}
return result;
};
Loading