Skip to content

Commit

Permalink
feat: sourcemap support (#153)
Browse files Browse the repository at this point in the history
Adds source map support for Chrome. Firefox and Safari don't currently work - along with HMR.
  • Loading branch information
Madou authored Apr 20, 2020
1 parent 10d15bd commit 9830560
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 36 deletions.
3 changes: 1 addition & 2 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<meta
http-equiv="Content-Security-Policy"
content=" object-src 'none'; style-src 'nonce-k0Mp1lEd'
'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:; base-uri 'none';"
content=" object-src 'none'; style-src 'nonce-k0Mp1lEd' 'unsafe-eval' https: http:; base-uri 'none';"
/>
2 changes: 1 addition & 1 deletion examples/class-names-dynamic-object.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const ObjectLiteral = () => {
<div>
<ClassNames>
{({ css, style }) => (
<div style={style} className={css({ color, fontSize: '30px' })}>
<div style={style} className={css({ color, fontSize: '40px' })}>
hello world
</div>
)}
Expand Down
3 changes: 3 additions & 0 deletions packages/ts-transform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
],
"dependencies": {
"@emotion/is-prop-valid": "^0.8.6",
"convert-source-map": "^1.7.0",
"source-map": "^0.7.3",
"stylis": "^3.5.4",
"stylis-rule-sheet": "^0.0.10",
"typescript": "^3.7.3"
},
"devDependencies": {
"@types/convert-source-map": "^1.5.1",
"ts-transformer-testing-library": "^1.0.0-alpha.7"
}
}
59 changes: 59 additions & 0 deletions packages/ts-transform/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,65 @@ describe('root transformer', () => {
}).not.toThrow();
});

it('should generate source maps for a css prop', () => {
const transformer = rootTransformer(stubProgam, { options: { sourceMap: true } });

const actual = ts.transpileModule(
`
import '@compiled/css-in-js';
<div css={{ fontSize: '20px' }}>hello world</div>
`,
createTsConfig(transformer)
);

expect(actual.outputText).toMatchInlineSnapshot(`
"import React from \\"react\\";
import { Style } from '@compiled/css-in-js';
<><Style hash=\\"1b1wq3m\\">{[\\".cc-1b1wq3m{font-size:20px;}\\\\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm1vZHVsZS50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRVEiLCJmaWxlIjoibW9kdWxlLnRzeCIsInNvdXJjZXNDb250ZW50IjpbIlxuICAgICAgICBpbXBvcnQgJ0Bjb21waWxlZC9jc3MtaW4tanMnO1xuICAgICAgICA8ZGl2IGNzcz17eyBmb250U2l6ZTogJzIwcHgnIH19PmhlbGxvIHdvcmxkPC9kaXY+XG4gICAgICAiXX0= */\\"]}</Style><div className=\\"cc-1b1wq3m\\">hello world</div></>;
"
`);
});

it('should generate source maps for a styled component', () => {
const transformer = rootTransformer(stubProgam, { options: { sourceMap: true } });

const actual = ts.transpileModule(
`
import { styled } from '@compiled/css-in-js';
styled.div({ fontSize: 20 });
`,
createTsConfig(transformer)
);

expect(actual.outputText).toMatchInlineSnapshot(`
"import React from \\"react\\";
import { Style } from '@compiled/css-in-js';
React.forwardRef(({ as: C = \\"div\\", ...props }, ref) => <><Style hash=\\"1b1wq3m\\">{[\\".cc-1b1wq3m{font-size:20px;}\\\\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm1vZHVsZS50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBR1EiLCJmaWxlIjoibW9kdWxlLnRzeCIsInNvdXJjZXNDb250ZW50IjpbIlxuICAgICAgICBpbXBvcnQgeyBzdHlsZWQgfSBmcm9tICdAY29tcGlsZWQvY3NzLWluLWpzJztcblxuICAgICAgICBzdHlsZWQuZGl2KHsgZm9udFNpemU6IDIwIH0pO1xuICAgICAgIl19 */\\"]}</Style><C {...props} ref={ref} className={\\"cc-1b1wq3m\\" + (props.className ? \\" \\" + props.className : \\"\\")}/></>);
"
`);
});

it('should generate source maps for a class names component', () => {
const transformer = rootTransformer(stubProgam, { options: { sourceMap: true } });

const actual = ts.transpileModule(
`
import { ClassNames } from '@compiled/css-in-js';
<ClassNames>{({ css }) => <div className={css({ fontSize: 20 })} />}</ClassNames>
`,
createTsConfig(transformer)
);

expect(actual.outputText).toMatchInlineSnapshot(`
"import React from \\"react\\";
import { Style } from '@compiled/css-in-js';
<><Style hash=\\"gpurwr\\">{[\\".cc-1b1wq3m{font-size:20px;}\\\\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm1vZHVsZS50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBR1EiLCJmaWxlIjoibW9kdWxlLnRzeCIsInNvdXJjZXNDb250ZW50IjpbIlxuICAgICAgICBpbXBvcnQgeyBDbGFzc05hbWVzIH0gZnJvbSAnQGNvbXBpbGVkL2Nzcy1pbi1qcyc7XG5cbiAgICAgICAgPENsYXNzTmFtZXM+eyh7IGNzcyB9KSA9PiA8ZGl2IGNsYXNzTmFtZT17Y3NzKHsgZm9udFNpemU6IDIwIH0pfSAvPn08L0NsYXNzTmFtZXM+XG4gICAgICAiXX0= */\\"]}</Style><div className={\\"cc-1b1wq3m\\"}/></>;
"
`);
});

it('should only import Style once', () => {
const transformer = rootTransformer(stubProgam, {});

Expand Down
8 changes: 7 additions & 1 deletion packages/ts-transform/src/class-names/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ export default function classNamesTransformer(
collectDeclarationsFromNode(node, program, collectedDeclarations);

if (isClassNameComponent(node)) {
return visitClassNamesJsxElement(node, context, collectedDeclarations, options);
return visitClassNamesJsxElement(
node,
context,
collectedDeclarations,
options,
sourceFile
);
}

return ts.visitEachChild(node, visitor, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const visitClassNamesJsxElement = (
classNamesNode: ts.JsxElement,
context: ts.TransformationContext,
collectedDeclarations: Declarations,
options: TransformerOptions
options: TransformerOptions,
sourceFile: ts.SourceFile
): ts.Node => {
let css = '';
let cssVariables: CssVariableExpressions[] = [];
Expand Down Expand Up @@ -93,13 +94,14 @@ export const visitClassNamesJsxElement = (
: returnNode.expression.body;

return createCompiledFragment(classNamesNode, {
...options,
css,
cssVariables,
children:
ts.isJsxElement(children) || ts.isJsxSelfClosingElement(children)
? children
: ts.createJsxExpression(undefined, children as any),
context,
nonce: options.nonce,
sourceFile,
});
};
8 changes: 7 additions & 1 deletion packages/ts-transform/src/css-prop/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ export default function cssPropTransformer(
collectDeclarationsFromNode(node, program, collectedDeclarations);

if (isJsxElementWithCssProp(node)) {
const newNode = visitJsxElementWithCssProp(node, collectedDeclarations, context, options);
const newNode = visitJsxElementWithCssProp(
node,
collectedDeclarations,
context,
options,
sourceFile
);

if (ts.isJsxSelfClosingElement(node)) {
// It was self closing - it can't have children!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export const visitJsxElementWithCssProp = (
node: ts.JsxElement | ts.JsxSelfClosingElement,
variableDeclarations: Declarations,
context: ts.TransformationContext,
options: TransformerOptions
options: TransformerOptions,
sourceFile: ts.SourceFile
) => {
logger.log('visiting a jsx element with a css prop');

Expand All @@ -53,9 +54,10 @@ export const visitJsxElementWithCssProp = (
const result = buildCss(getNodeToExtract(cssProp), variableDeclarations, context);

return createCompiledComponentFromNode(node, {
...options,
...result,
sourceFile,
context,
propsToRemove: [CSS_PROP_NAME],
nonce: options.nonce,
...result,
});
};
2 changes: 1 addition & 1 deletion packages/ts-transform/src/styled-component/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function styledComponentTransformer(
}

if (isStyledComponent(node)) {
return visitStyledComponent(node, context, collectedDeclarations, options);
return visitStyledComponent(node, context, collectedDeclarations, options, sourceFile);
}

return ts.visitEachChild(node, visitor, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export const visitStyledComponent = (
node: ts.CallExpression | ts.TaggedTemplateExpression,
context: ts.TransformationContext,
collectedDeclarations: Declarations,
options: TransformerOptions
options: TransformerOptions,
sourceFile: ts.SourceFile
): ts.Node => {
const originalTagName = getTagName(node);
const result = buildCss(getCssNode(node), collectedDeclarations, context);
Expand Down Expand Up @@ -94,11 +95,12 @@ export const visitStyledComponent = (
});

const newElement = createCompiledComponent(ts.createIdentifier(constants.STYLED_AS_USAGE_NAME), {
...options,
css: result.css,
cssVariables: visitedCssVariables,
node,
context,
nonce: options.nonce,
sourceFile,
styleFactory: props => [
ts.createSpreadAssignment(ts.createIdentifier('props.style')),
...props.map(prop => {
Expand Down
1 change: 1 addition & 0 deletions packages/ts-transform/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export interface ToCssReturnType {
export interface TransformerOptions {
nonce?: string;
debug?: boolean;
sourceMap?: boolean;
}
19 changes: 18 additions & 1 deletion packages/ts-transform/src/utils/create-jsx-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { joinToJsxExpression } from './expression-operators';
import { CssVariableExpressions } from '../types';
import * as constants from '../constants';
import { concatArrays } from './functional-programming';
import { getSourceMap } from './source-maps';

interface JsxElementOpts {
css: string;
Expand All @@ -20,6 +21,8 @@ interface JsxElementOpts {
children?: ts.JsxChild;
context: ts.TransformationContext;
nonce?: string;
sourceMap?: boolean;
sourceFile: ts.SourceFile;
}

function stripPrefix(className: string) {
Expand All @@ -37,6 +40,15 @@ const createStyleNode = (node: ts.Node, className: string, css: string[], opts:
]
: [];

const sourceMap = opts.sourceMap
? '\n' +
getSourceMap(
opts.sourceFile.getLineAndCharacterOfPosition(node.getStart()),
opts.sourceFile,
opts.context
)
: '';

return ts.createJsxElement(
// We use setOriginalNode() here to work around createJsx not working without the original node.
// See: https://github.com/microsoft/TypeScript/issues/35686
Expand All @@ -60,7 +72,12 @@ const createStyleNode = (node: ts.Node, className: string, css: string[], opts:
ts.createJsxExpression(
undefined,
ts.createArrayLiteral(
css.map(rule => ts.createStringLiteral(rule)),
/**
* Each source map is tied to a specific CSS block (each CSS block/declaration is one element of the array).
* Ends up looking like: `.cc-1b1wq3m{font-size:20px;}\n/*# sourceMappingURL=...`
* When source maps are turn on.
*/
css.map(rule => ts.createStringLiteral(rule + sourceMap)),
false
)
),
Expand Down
46 changes: 46 additions & 0 deletions packages/ts-transform/src/utils/source-maps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as ts from 'typescript';
import { SourceMapGenerator } from 'source-map';
import convert from 'convert-source-map';
import path from 'path';

const getFileName = (sourceFile: ts.SourceFile): string => {
return path.basename(sourceFile.fileName);
};

/**
* Used to generate a inline source map for CSS.
* It's input is the TypeScript source file,
* an offset (where we should place the cursor when jumping to the source map)
* and TypeScript context.
*
* Will return something like `/*# sourceMappingURL=...`
*/
export function getSourceMap(
offset: {
line: number;
character: number;
},
sourceFile: ts.SourceFile,
context: ts.TransformationContext
): string {
const fileName = getFileName(sourceFile);
const generator = new SourceMapGenerator({
file: fileName,
sourceRoot: context.getCompilerOptions().sourceRoot,
});

generator.setSourceContent(fileName, sourceFile.getFullText());
generator.addMapping({
generated: {
line: 1,
column: 0,
},
source: fileName,
original: {
line: offset.line + 1,
column: offset.character,
},
});

return convert.fromObject(generator).toComment({ multiline: true });
}
36 changes: 14 additions & 22 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,11 @@
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"

"@types/convert-source-map@^1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@types/convert-source-map/-/convert-source-map-1.5.1.tgz#d4d180dd6adc5cb68ad99bd56e03d637881f4616"
integrity sha512-laiDIXqqthjJlyAMYAXOtN3N8+UlbM+KvZi4BaY5ZOekmVkBs/UxfK5O0HWeJVG2eW8F+Mu2ww13fTX+kY1FlQ==

"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
Expand Down Expand Up @@ -3596,15 +3601,11 @@ core-js-compat@^3.6.2:
browserslist "^4.8.3"
semver "7.0.0"

core-js-pure@^3.0.0:
core-js-pure@^3.0.0, core-js-pure@^3.0.1:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==

core-js-pure@^3.0.1:
version "3.6.4"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"

core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
Expand Down Expand Up @@ -8945,11 +8946,7 @@ regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"

regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3:
version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"

regenerator-runtime@^0.13.4:
regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
Expand Down Expand Up @@ -9122,13 +9119,7 @@ [email protected]:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"

[email protected], resolve@^1.0.0, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.9.0:
version "1.14.2"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2"
dependencies:
path-parse "^1.0.6"

resolve@^1.15.1:
[email protected], resolve@^1.0.0, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.9.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.16.0.tgz#063dc704fa3413e13ac1d0d1756a7cbfe95dd1a7"
integrity sha512-LarL/PIKJvc09k1jaeT4kQb/8/7P+qV4qSnN2K80AES+OHdfZELAKVOBjxsvtToT/uLOfFbvYvKfZmV8cee7nA==
Expand Down Expand Up @@ -9590,6 +9581,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"

source-map@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==

space-separated-tokens@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.4.tgz#27910835ae00d0adfcdbd0ad7e611fb9544351fa"
Expand Down Expand Up @@ -10355,15 +10351,11 @@ tsconfig@^7.0.0:
strip-bom "^3.0.0"
strip-json-comments "^2.0.0"

tslib@^1.8.1:
tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==

tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"

tsutils@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
Expand Down

0 comments on commit 9830560

Please sign in to comment.