Skip to content

Commit

Permalink
Implement CSS sourcemaps
Browse files Browse the repository at this point in the history
  • Loading branch information
Will Binns-Smith committed Apr 6, 2020
1 parent d98c9c3 commit ad88371
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 38 deletions.
21 changes: 12 additions & 9 deletions flow-libs/postcss.js.flow
Expand Up @@ -24,20 +24,23 @@ declare module 'postcss' {
declare interface Root extends Container {}

declare class Processor {
process(css: string | Result, opts?: processOptions): Promise<Result>;
process(
css: string | Result | Root,
opts?: ProcessOptions,
): Promise<Result>;
}

declare type ProcessOptions = {|
from?: string,
to?: string,
map?: MapOptions,
parser?: parser,
stringifier?: stringifier,
syntax?: {|
declare type ProcessOptions = $Shape<{|
from: string,
to: string,
map: MapOptions,
parser: parser,
stringifier: stringifier,
syntax: {|
parser: parser,
stringifier: stringifier,
|},
|};
|}>;

declare type MapOptions = {|
inline?: boolean,
Expand Down
24 changes: 23 additions & 1 deletion packages/core/integration-tests/test/css.js
Expand Up @@ -264,6 +264,7 @@ describe('css', () => {
path.join(__dirname, '/integration/cssnano/index.js'),
{
minify: true,
sourceMap: false,
},
);

Expand All @@ -275,10 +276,31 @@ describe('css', () => {
assert(css.includes('.local'));
assert(css.includes('.index'));

// TODO: Make this `2` when a `sourceMappingURL` is added
assert.equal(css.split('\n').length, 1);
});

it('should produce a sourcemap when sourceMap is set', async function() {
await bundle(path.join(__dirname, '/integration/cssnano/index.js'), {
minify: true,
});

let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8');
assert(css.includes('.local'));
assert(css.includes('.index'));

let lines = css.trim().split('\n');
assert.equal(lines.length, 2);
assert.equal(lines[1], '/*# sourceMappingURL=index.css.map */');

let map = JSON.parse(
await outputFS.readFile(path.join(distDir, 'index.css.map'), 'utf8'),
);
assert.deepEqual(map.sources, [
'integration/cssnano/local.css',
'integration/cssnano/index.css',
]);
});

it('should inline data-urls for text-encoded files', async () => {
await bundle(path.join(__dirname, '/integration/data-url/text.css'));
let css = await outputFS.readFile(path.join(distDir, 'text.css'), 'utf8');
Expand Down
1 change: 1 addition & 0 deletions packages/optimizers/cssnano/package.json
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@parcel/plugin": "^2.0.0-alpha.3.1",
"@parcel/source-map": "^2.0.0-alpha.4.6",
"cssnano": "^4.1.10",
"postcss": "^7.0.5"
}
Expand Down
52 changes: 43 additions & 9 deletions packages/optimizers/cssnano/src/CSSNanoOptimizer.js
@@ -1,32 +1,66 @@
// @flow strict-local

import SourceMap from '@parcel/source-map';
import {Optimizer} from '@parcel/plugin';
import postcss from 'postcss';
// flowlint-next-line untyped-import:off
import cssnano from 'cssnano';

const sentinelPath = '@@parcel-cssnano-optimizer';

export default new Optimizer({
async optimize({bundle, contents, map}) {
async optimize({
bundle,
contents: prevContents,
getSourceMapReference,
map: prevMap,
options,
}) {
if (!bundle.env.minify) {
return {contents, map};
return {contents: prevContents, map: prevMap};
}

if (typeof contents !== 'string') {
if (typeof prevContents !== 'string') {
throw new Error(
'CSSNanoOptimizer: Only string contents are currently supported',
);
}

const results = await postcss([cssnano]).process(contents, {
from: bundle.filePath,
map: {inline: false},
const result = await postcss([cssnano]).process(prevContents, {
// Postcss uses a `from` path and seems to include it as a source.
// In our case, the previous map contains all the needed sources.
// Provide a known sentinel path to use as the source, and filter it out
// from the produced sources below.
from: sentinelPath,
map: {
annotation: false,
inline: false,
prev: prevMap ? await prevMap.stringify({}) : null,
},
});

console.log('MAP', results.map.constructor);
let map;
if (result.map != null) {
map = new SourceMap();
let {mappings, sources, names} = result.map.toJSON();
map.addRawMappings(
mappings,
sources.filter(source => source !== sentinelPath),
names,
);
}

let contents = result.css;
if (options.sourceMaps) {
let reference = await getSourceMapReference(map);
if (reference != null) {
contents += '\n' + '/*# sourceMappingURL=' + reference + ' */\n';
}
}

return {
contents: results.css,
map: results.map,
contents,
map,
};
},
});
1 change: 1 addition & 0 deletions packages/packagers/css/package.json
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@parcel/plugin": "^2.0.0-alpha.3.1",
"@parcel/source-map": "^2.0.0-alpha.4.6",
"@parcel/utils": "^2.0.0-alpha.3.1"
}
}
70 changes: 58 additions & 12 deletions packages/packagers/css/src/CSSPackager.js
@@ -1,14 +1,23 @@
// @flow

import path from 'path';
import SourceMap from '@parcel/source-map';
import {Packager} from '@parcel/plugin';
import {
PromiseQueue,
countLines,
replaceInlineReferences,
replaceURLReferences,
} from '@parcel/utils';

export default new Packager({
async package({bundle, bundleGraph, getInlineBundleContents}) {
async package({
bundle,
bundleGraph,
getInlineBundleContents,
options,
getSourceMapReference,
}) {
let queue = new PromiseQueue({maxConcurrent: 32});
bundle.traverseAssets({
exit: asset => {
Expand All @@ -25,31 +34,68 @@ export default new Packager({
}

queue.add(() =>
asset.getCode().then((css: string) => {
if (media.length) {
return `@media ${media.join(', ')} {\n${css.trim()}\n}\n`;
}
Promise.all([
asset,
asset.getCode().then((css: string) => {
if (media.length) {
return `@media ${media.join(', ')} {\n${css}\n}\n`;
}

return css;
}),
return css;
}),
options.sourceMaps && asset.getMapBuffer(),
]),
);
},
});

let outputs = await queue.run();
let contents = '';
let map = new SourceMap();
let lineOffset = 0;
for (let [asset, code, mapBuffer] of outputs) {
contents += code + '\n';
if (options.sourceMaps) {
if (mapBuffer) {
map.addBufferMappings(mapBuffer, lineOffset);
} else {
map.addEmptyMap(
path
.relative(options.projectRoot, asset.filePath)
.replace(/\\+/g, '/'),
code,
lineOffset,
);
}

lineOffset += countLines(code) + 1;
}
}

if (options.sourceMaps) {
let reference = await getSourceMapReference(map);
if (reference != null) {
contents += '/*# sourceMappingURL=' + reference + ' */\n';
}
}

({contents, map} = replaceURLReferences({
bundle,
bundleGraph,
contents,
map,
}));

return replaceInlineReferences({
bundle,
bundleGraph,
contents: replaceURLReferences({
bundle,
bundleGraph,
contents: outputs.map(output => output).join('\n'),
}).contents,
contents,
getInlineBundleContents,
getInlineReplacement: (dep, inlineType, contents) => ({
from: dep.id,
to: contents,
}),
map,
});
},
});
3 changes: 1 addition & 2 deletions packages/packagers/js/src/JSPackager.js
Expand Up @@ -136,7 +136,6 @@ export default new Packager({
wrapped += ']';

if (options.sourceMaps) {
let lineCount = countLines(output);
if (mapBuffer) {
map.addBufferMappings(mapBuffer, lineOffset);
} else {
Expand All @@ -149,7 +148,7 @@ export default new Packager({
);
}

lineOffset += lineCount + 1;
lineOffset += countLines(output) + 1;
}
i++;
}
Expand Down
1 change: 1 addition & 0 deletions packages/transformers/css/package.json
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@parcel/plugin": "^2.0.0-alpha.3.1",
"@parcel/source-map": "^2.0.0-alpha.4.6",
"@parcel/utils": "^2.0.0-alpha.3.1",
"postcss": "^7.0.5",
"postcss-value-parser": "^3.3.1",
Expand Down
25 changes: 20 additions & 5 deletions packages/transformers/css/src/CSSTransformer.js
Expand Up @@ -3,6 +3,7 @@
import type {FilePath} from '@parcel/types';
import type {Container, Node} from 'postcss';

import SourceMap from '@parcel/source-map';
import {Transformer} from '@parcel/plugin';
import {createDependencyLocation, isURL} from '@parcel/utils';
import postcss from 'postcss';
Expand Down Expand Up @@ -166,7 +167,7 @@ export default new Transformer({
return [asset];
},

generate({ast}) {
async generate({asset, ast}) {
let root = ast.program;

// $FlowFixMe
Expand All @@ -192,13 +193,27 @@ export default new Transformer({
root.each((node, index) => convert(root, node, index));
}

let code = '';
postcss.stringify(root, c => {
code += c;
let result = await postcss().process(root, {
from: asset.filePath,
map: {
annotation: false,
inline: false,
},
// Pass postcss's own stringifier to it to silence its warning
// as we don't want to perform any transformations -- only generate
stringifier: postcss.stringify,
});

let map;
if (result.map != null) {
map = new SourceMap();
let {mappings, sources, names} = result.map.toJSON();
map.addRawMappings(mappings, sources, names);
}

return {
content: code,
content: result.css,
map,
};
},
});

0 comments on commit ad88371

Please sign in to comment.