From 9830560bb3e805b77ae05aadc4b139b79cb95536 Mon Sep 17 00:00:00 2001 From: DOUGES Date: Mon, 20 Apr 2020 22:02:39 +1000 Subject: [PATCH] feat: sourcemap support (#153) Adds source map support for Chrome. Firefox and Safari don't currently work - along with HMR. --- .storybook/preview-head.html | 3 +- examples/class-names-dynamic-object.tsx | 2 +- packages/ts-transform/package.json | 3 + .../ts-transform/src/__tests__/index.test.tsx | 59 +++++++++++++++++++ .../ts-transform/src/class-names/index.tsx | 8 ++- .../visit-class-names-jsx-element.tsx | 6 +- packages/ts-transform/src/css-prop/index.tsx | 8 ++- .../visit-jsx-element-with-css-prop.tsx | 8 ++- .../src/styled-component/index.tsx | 2 +- .../visitors/visit-styled-component.tsx | 6 +- packages/ts-transform/src/types.tsx | 1 + .../src/utils/create-jsx-element.tsx | 19 +++++- .../ts-transform/src/utils/source-maps.tsx | 46 +++++++++++++++ yarn.lock | 36 +++++------ 14 files changed, 171 insertions(+), 36 deletions(-) create mode 100644 packages/ts-transform/src/utils/source-maps.tsx diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 558dcaffe..ddf31e6a8 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -1,5 +1,4 @@ diff --git a/examples/class-names-dynamic-object.tsx b/examples/class-names-dynamic-object.tsx index a8b0d8436..e25f0eb0a 100644 --- a/examples/class-names-dynamic-object.tsx +++ b/examples/class-names-dynamic-object.tsx @@ -12,7 +12,7 @@ export const ObjectLiteral = () => {
{({ css, style }) => ( -
+
hello world
)} diff --git a/packages/ts-transform/package.json b/packages/ts-transform/package.json index 4bdb7afd7..196995743 100644 --- a/packages/ts-transform/package.json +++ b/packages/ts-transform/package.json @@ -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" } } diff --git a/packages/ts-transform/src/__tests__/index.test.tsx b/packages/ts-transform/src/__tests__/index.test.tsx index 051f22005..9c0fc72f4 100644 --- a/packages/ts-transform/src/__tests__/index.test.tsx +++ b/packages/ts-transform/src/__tests__/index.test.tsx @@ -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'; +
hello world
+ `, + createTsConfig(transformer) + ); + + expect(actual.outputText).toMatchInlineSnapshot(` + "import React from \\"react\\"; + import { Style } from '@compiled/css-in-js'; + <>
hello world
; + " + `); + }); + + 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) => <>); + " + `); + }); + + 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'; + + {({ css }) =>
} + `, + createTsConfig(transformer) + ); + + expect(actual.outputText).toMatchInlineSnapshot(` + "import React from \\"react\\"; + import { Style } from '@compiled/css-in-js'; + <>
; + " + `); + }); + it('should only import Style once', () => { const transformer = rootTransformer(stubProgam, {}); diff --git a/packages/ts-transform/src/class-names/index.tsx b/packages/ts-transform/src/class-names/index.tsx index f9b0b17f4..01bc600c6 100644 --- a/packages/ts-transform/src/class-names/index.tsx +++ b/packages/ts-transform/src/class-names/index.tsx @@ -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); diff --git a/packages/ts-transform/src/class-names/visitors/visit-class-names-jsx-element.tsx b/packages/ts-transform/src/class-names/visitors/visit-class-names-jsx-element.tsx index d99db750c..b811a3923 100644 --- a/packages/ts-transform/src/class-names/visitors/visit-class-names-jsx-element.tsx +++ b/packages/ts-transform/src/class-names/visitors/visit-class-names-jsx-element.tsx @@ -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[] = []; @@ -93,6 +94,7 @@ export const visitClassNamesJsxElement = ( : returnNode.expression.body; return createCompiledFragment(classNamesNode, { + ...options, css, cssVariables, children: @@ -100,6 +102,6 @@ export const visitClassNamesJsxElement = ( ? children : ts.createJsxExpression(undefined, children as any), context, - nonce: options.nonce, + sourceFile, }); }; diff --git a/packages/ts-transform/src/css-prop/index.tsx b/packages/ts-transform/src/css-prop/index.tsx index 7c913055e..f629aa10d 100644 --- a/packages/ts-transform/src/css-prop/index.tsx +++ b/packages/ts-transform/src/css-prop/index.tsx @@ -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! diff --git a/packages/ts-transform/src/css-prop/visitors/visit-jsx-element-with-css-prop.tsx b/packages/ts-transform/src/css-prop/visitors/visit-jsx-element-with-css-prop.tsx index 213882d6b..90f15e802 100644 --- a/packages/ts-transform/src/css-prop/visitors/visit-jsx-element-with-css-prop.tsx +++ b/packages/ts-transform/src/css-prop/visitors/visit-jsx-element-with-css-prop.tsx @@ -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'); @@ -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, }); }; diff --git a/packages/ts-transform/src/styled-component/index.tsx b/packages/ts-transform/src/styled-component/index.tsx index d99303a65..c8c602974 100644 --- a/packages/ts-transform/src/styled-component/index.tsx +++ b/packages/ts-transform/src/styled-component/index.tsx @@ -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); diff --git a/packages/ts-transform/src/styled-component/visitors/visit-styled-component.tsx b/packages/ts-transform/src/styled-component/visitors/visit-styled-component.tsx index 21a6cbcb8..6df7ab78e 100644 --- a/packages/ts-transform/src/styled-component/visitors/visit-styled-component.tsx +++ b/packages/ts-transform/src/styled-component/visitors/visit-styled-component.tsx @@ -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); @@ -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 => { diff --git a/packages/ts-transform/src/types.tsx b/packages/ts-transform/src/types.tsx index b24cace84..2466bc8d1 100644 --- a/packages/ts-transform/src/types.tsx +++ b/packages/ts-transform/src/types.tsx @@ -24,4 +24,5 @@ export interface ToCssReturnType { export interface TransformerOptions { nonce?: string; debug?: boolean; + sourceMap?: boolean; } diff --git a/packages/ts-transform/src/utils/create-jsx-element.tsx b/packages/ts-transform/src/utils/create-jsx-element.tsx index 42a72a344..19742e403 100644 --- a/packages/ts-transform/src/utils/create-jsx-element.tsx +++ b/packages/ts-transform/src/utils/create-jsx-element.tsx @@ -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; @@ -20,6 +21,8 @@ interface JsxElementOpts { children?: ts.JsxChild; context: ts.TransformationContext; nonce?: string; + sourceMap?: boolean; + sourceFile: ts.SourceFile; } function stripPrefix(className: string) { @@ -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 @@ -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 ) ), diff --git a/packages/ts-transform/src/utils/source-maps.tsx b/packages/ts-transform/src/utils/source-maps.tsx new file mode 100644 index 000000000..54e268851 --- /dev/null +++ b/packages/ts-transform/src/utils/source-maps.tsx @@ -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 }); +} diff --git a/yarn.lock b/yarn.lock index 0a7e85e2c..63f66466e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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== @@ -9122,13 +9119,7 @@ resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@1.x, 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: +resolve@1.x, 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== @@ -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" @@ -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"