Skip to content

Commit

Permalink
Feature/handle extracted compiled files (#1513)
Browse files Browse the repository at this point in the history
* Extract styles to file in babel plugin

* Add changeset

* Add test for extracting to css files

* WIP

* Remove parcel-transformer-css reference

* Fix type errors

* Add .synk

* Update .synk

* Update .synk

* Rename .synk to snyk (silly me)

* Update webpack plugin

* Update Parcel integration test

* Remove duplicates

* Update snapshot test

* mute ts error in the fixture

* Specify file name

* Refactor tests

* Add .css to prettierignore

* remove .only

* PR feedbacks

* Add extractStylesToDirectory option to Webpack and Parcel

* escape chars

* Use AST contruction instead of template.ast

---------

Co-authored-by: Jake Lane <[email protected]>
  • Loading branch information
liamqma and JakeLane authored Oct 4, 2023
1 parent a49e950 commit 9304fa3
Show file tree
Hide file tree
Showing 32 changed files with 469 additions and 179 deletions.
7 changes: 7 additions & 0 deletions .changeset/fuzzy-walls-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@compiled/babel-plugin-strip-runtime': minor
'@compiled/parcel-transformer': patch
'@compiled/webpack-loader': patch
---

Add extractStylesToDirectory config to support extraction to CSS files
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ CHANGELOG.md
coverage
dist
storybook-static
**/__fixtures__/*.css
2 changes: 1 addition & 1 deletion examples/parcel/.compiledcssrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"importReact": false,
"importReact": true,
"extensions": [".js", ".jsx", ".ts", ".tsx", ".customjsx"],
"parserBabelPlugins": ["typescript", "jsx"],
"transformerBabelPlugins": [
Expand Down
5 changes: 4 additions & 1 deletion examples/parcel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
"private": true,
"scripts": {
"build": "parcel build ./src/index.html",
"start": "parcel serve ./src/index.html --open --no-cache"
"start": "parcel serve ./src/index.html --open --no-cache",
"start:debug": "node --inspect-brk node_modules/.bin/parcel serve ./src/index.html --open --no-cache"
},
"dependencies": {
"@compiled/babel-component-extracted-fixture": "*",
"@compiled/react": "*",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@babel/plugin-proposal-decorators": "^7.22.15",
"@compiled/parcel-config": "*",
"@compiled/parcel-transformer": "*",
"@parcel/config-default": "^2.8.3",
Expand Down
11 changes: 6 additions & 5 deletions examples/parcel/src/app.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import '@compiled/react';
import BabelComponentExtracted from '@compiled/babel-component-extracted-fixture/dist/index';
import { css } from '@compiled/react';
import React, { lazy } from 'react';
// These are unused placeholder examples, as including them will break the application. The static evaluation in the
// @compiled/babel-plugin must be synchronous, whereas parcel offers promise-based APIs, making them incompatible.
// Eventually, the static evaluation (i.e. resolveBindingNode) should be replaced or removed so that these aliases
Expand All @@ -16,14 +17,14 @@ import {
} from './ui/custom-file-extension.customjsx';
import { TypeScript } from './ui/typescript';

const AsyncComponent = React.lazy(() => import('./async'));

const AsyncComponent = lazy(() => import('./async'));
export const App = () => (
<>
<div css={{ fontSize: 50, color: primary }}>hello from parcel 2</div>
<div css={css({ fontSize: 50, color: primary })}>hello from parcel 2</div>
<TypeScript color="blue" />
{/*<div css={parcelAliasStyles}>custom alias</div>*/}
{/*<div css={parcelResolverAliasStyles}>custom resolver</div>*/}
<BabelComponentExtracted>Component from NPM</BabelComponentExtracted>
<CustomFileExtensionStyled>Custom File Extension Styled</CustomFileExtensionStyled>
<div css={customFileExtensionCss}>Custom File Extension CSS</div>
<Annotated />
Expand Down
1 change: 1 addition & 0 deletions examples/webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"start": "webpack serve --hot"
},
"dependencies": {
"@compiled/babel-component-extracted-fixture": "*",
"@compiled/babel-component-fixture": "*",
"@compiled/react": "*",
"@compiled/webpack-loader": "*",
Expand Down
6 changes: 4 additions & 2 deletions examples/webpack/src/app.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BabelComponentExtracted from '@compiled/babel-component-extracted-fixture/dist/index';
import BabelComponent from '@compiled/babel-component-fixture';
import '@compiled/react';
import { css } from '@compiled/react';
import { Suspense, lazy } from 'react';

import { primary } from './common/constants';
Expand All @@ -15,12 +16,13 @@ const AsyncComponent = lazy(() => import('./ui/async'));

export const App = () => (
<>
<div css={{ fontSize: 50, color: primary }}>hello from webpack</div>
<div css={css({ fontSize: 50, color: primary })}>hello from webpack</div>
<TypeScript>TypeScript component</TypeScript>
<BabelComponent>Component from NPM</BabelComponent>
<Suspense fallback="Loading...">
<AsyncComponent>I was loaded async</AsyncComponent>
</Suspense>
<BabelComponentExtracted>Component from NPM</BabelComponentExtracted>
<CustomFileExtensionStyled>Custom File Extension Styled</CustomFileExtensionStyled>
<div css={customFileExtensionCss}>Custom File Extension CSS</div>
<Annotated />
Expand Down
13 changes: 13 additions & 0 deletions fixtures/babel-component-extracted/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"presets": [
["@babel/preset-env", { "targets": { "browsers": "last 1 version" } }],
["@babel/preset-react", { "runtime": "automatic" }]
],
"plugins": [
["@compiled/babel-plugin", { "importReact": false, "optimizeCss": false }],
[
"@compiled/babel-plugin-strip-runtime",
{ "extractStylesToDirectory": { "source": "src", "dest": "dist" } }
]
]
}
23 changes: 23 additions & 0 deletions fixtures/babel-component-extracted/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@compiled/babel-component-extracted-fixture",
"version": "0.1.0",
"private": true,
"main": "./dist/index.js",
"scripts": {
"build": "TEST_PKG_VERSION='0.0.0' babel ./src --out-dir=./dist"
},
"dependencies": {
"@compiled/react": "*"
},
"devDependencies": {
"@babel/cli": "^7.21.5",
"@babel/core": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.18.6",
"@compiled/babel-plugin": "*",
"@compiled/babel-plugin-strip-runtime": "*"
},
"peerDependencies": {
"react": "^17.0.1"
}
}
17 changes: 17 additions & 0 deletions fixtures/babel-component-extracted/src/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { styled, css } from '@compiled/react';

const Button = styled.button({
color: 'blue',
fontSize: '30px',
border: '2px solid blue',
padding: '32px',
backgroundColor: 'yellow',
});

export default function BabelComponent({ children }) {
return (
<div css={css({ marginTop: 30 })}>
<Button>{children}</Button>
</div>
);
}
1 change: 1 addition & 0 deletions fixtures/parcel-transformer-test-extract-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"react-dom": "^17.0.2"
},
"devDependencies": {
"@compiled/babel-component-extracted-fixture": "*",
"@compiled/babel-component-fixture": "*",
"@compiled/parcel-transformer": "*",
"@parcel/config-default": "^2.8.3"
Expand Down
6 changes: 4 additions & 2 deletions fixtures/parcel-transformer-test-extract-app/src/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import BabelComponentExtracted from '@compiled/babel-component-extracted-fixture/dist/index';
import BabelComponent from '@compiled/babel-component-fixture';
import '@compiled/react';
import { css } from '@compiled/react';

const App = () => (
<>
<div css={{ fontSize: 50, color: 'red' }}>CSS prop</div>
<div css={css({ fontSize: 50, color: 'blue' })}>CSS prop</div>
<BabelComponent>Babel component</BabelComponent>
<BabelComponentExtracted>Component from NPM</BabelComponentExtracted>
</>
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"benchmark": "jest --config benchmark.config.json",
"build": "yarn build:babel-fixture && yarn build:browser && yarn build:cjs && yarn build:esm && yarn flow-types build && yarn postbuild",
"postbuild": "scripts/postbuild.sh",
"build:babel-fixture": "yarn workspace @compiled/babel-component-fixture build",
"build:babel-fixture": "yarn workspace @compiled/babel-component-fixture build && yarn workspace @compiled/babel-component-extracted-fixture build",
"build:browser": "ttsc --build packages/tsconfig.browser.json",
"build:cjs": "ttsc --build packages/tsconfig.cjs.json",
"build:esm": "ttsc --build packages/tsconfig.json",
Expand Down
1 change: 1 addition & 0 deletions packages/babel-plugin-strip-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@babel/template": "^7.20.7",
"@babel/traverse": "^7.21.5",
"@babel/types": "^7.21.5",
"@compiled/css": "^0.12.0",
"@compiled/utils": "^0.8.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { join } from 'path';

import { transformSync as babelTransformSync } from '@babel/core';
import type { BabelFileResult } from '@babel/core';
import compiledBabelPlugin from '@compiled/babel-plugin';
Expand All @@ -8,6 +6,10 @@ import { format } from 'prettier';
import stripRuntimeBabelPlugin from '../index';
import type { BabelFileMetadata } from '../types';

// Mock out FS to avoid writing to disk
// We aren't processing the result anyway, so no need for specifying the response
jest.mock('fs');

const testStyleSheetPath =
'@compiled/webpack-loader/css-loader!@compiled/webpack-loader/css-loader/compiled-css.css';
const regexToFindRequireStatements =
Expand All @@ -21,21 +23,32 @@ const transformSync = (
compiledRequireExclude?: boolean;
run: 'both' | 'bake' | 'extract';
runtime: 'automatic' | 'classic';
extractStylesToDirectory?: { source: string; dest: string };
}
): BabelFileResult | null => {
const { styleSheetPath, compiledRequireExclude, run, runtime } = opts;
const { styleSheetPath, compiledRequireExclude, run, runtime, extractStylesToDirectory } = opts;
const bake = run === 'both' || run === 'bake';
const extract = run === 'both' || run === 'extract';

return babelTransformSync(code, {
babelrc: false,
configFile: false,
filename: join(__dirname, 'app.tsx'),
filename: '/base/src/app.tsx',
generatorOpts: {
sourceFileName: '../src/app.tsx',
},
plugins: [
...(bake
? [[compiledBabelPlugin, { importReact: runtime === 'classic', optimizeCss: false }]]
: []),
...(extract ? [[stripRuntimeBabelPlugin, { styleSheetPath, compiledRequireExclude }]] : []),
...(extract
? [
[
stripRuntimeBabelPlugin,
{ styleSheetPath, compiledRequireExclude, extractStylesToDirectory },
],
]
: []),
],
presets: [['@babel/preset-react', { runtime }]],
});
Expand All @@ -48,6 +61,7 @@ const transform = (
compiledRequireExclude?: boolean;
run: 'both' | 'bake' | 'extract';
runtime: 'automatic' | 'classic';
extractStylesToDirectory?: { source: string; dest: string };
}
): string => {
const fileResult = transformSync(c, opts);
Expand Down Expand Up @@ -195,6 +209,43 @@ describe('babel-plugin-strip-runtime using source code', () => {
styleRules: ['._1wyb1fwx{font-size:12px}', '._syaz13q2{color:blue}'],
});
});

it('adds styles to directory', () => {
const actual = transform(code, {
run: 'both',
runtime,
extractStylesToDirectory: { source: 'src/', dest: 'dist/' },
});

expect(actual).toMatchInlineSnapshot(`
"/* app.tsx generated by @compiled/babel-plugin v0.0.0 */
import './app.compiled.css';
import * as React from 'react';
import { ax, ix } from '@compiled/react/runtime';
const Component = () =>
/*#__PURE__*/ React.createElement(
'div',
{
className: ax(['_1wyb1fwx _syaz13q2']),
},
'hello world'
);
"
`);
});

it('error when source directory is not found', () => {
expect(() =>
transform(code, {
run: 'both',
runtime,
extractStylesToDirectory: { source: 'not-existing-src/', dest: 'dist/' },
})
).toThrowWithMessage(
Error,
`/base/src/app.tsx: Source directory 'not-existing-src/' was not found relative to source file ('../src/app.tsx')`
);
});
});
});

Expand Down
45 changes: 43 additions & 2 deletions packages/babel-plugin-strip-runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { mkdirSync, writeFileSync } from 'fs';
import { dirname, join, parse } from 'path';

import { declare } from '@babel/helper-plugin-utils';
import template from '@babel/template';
import type { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
import { sort } from '@compiled/css';
import { preserveLeadingComments } from '@compiled/utils';

import type { PluginPass, PluginOptions, BabelFileMetadata } from './types';
Expand All @@ -21,9 +25,9 @@ export default declare<PluginPass>((api) => {
},
visitor: {
Program: {
exit(path, { file }) {
exit(path, { file, filename }) {
if (this.opts.compiledRequireExclude) {
// Rather than inserting styleRules to the code, inserting them to matadata in the case like SSR
// Rather than inserting styleRules to the code, inserting them to metadata in the case like SSR
if (!file.metadata?.styleRules) file.metadata.styleRules = [];
this.styleRules.forEach((rule) => {
file.metadata.styleRules.push(rule);
Expand All @@ -47,6 +51,43 @@ export default declare<PluginPass>((api) => {
// If we used ESM it would blow up with CJS source, unfortunately.
});
}

if (this.opts.extractStylesToDirectory && this.styleRules.length > 0) {
// Build and sanitize filename of the css file
const cssFilename = `${parse(filename).name}.compiled.css`;

if (!file.opts.generatorOpts?.sourceFileName) {
throw new Error(`Source filename was not defined`);
}
const sourceFileName = file.opts.generatorOpts.sourceFileName;
if (!sourceFileName.includes(this.opts.extractStylesToDirectory.source)) {
throw new Error(
`Source directory '${this.opts.extractStylesToDirectory.source}' was not found relative to source file ('${sourceFileName}')`
);
}

// Get the path relative to the working directory
const relativePath = sourceFileName.slice(
sourceFileName.indexOf(this.opts.extractStylesToDirectory.source) +
this.opts.extractStylesToDirectory.source.length
);

// Write styles to sibling css file
const cssFilePath = join(
this.cwd,
this.opts.extractStylesToDirectory.dest,
dirname(relativePath),
cssFilename
);
mkdirSync(dirname(cssFilePath), { recursive: true });
writeFileSync(cssFilePath, sort(this.styleRules.sort().join('\n')));

// Add css import to file
path.unshiftContainer(
'body',
t.importDeclaration([], t.stringLiteral(`./${cssFilename}`))
);
}
},
},
ImportSpecifier(path) {
Expand Down
Loading

0 comments on commit 9304fa3

Please sign in to comment.