Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add style tag injection #659

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 25 additions & 7 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ module.exports = {

**note** invalid characters will be replaced with an underscore (`_`).

- `injectStyleTags: { preprocessor: Preprocessor } | true | false` (default: `false`)

Using this option will transform your file to your file to create a CSS style tag when your module is imported.

e.x.

```javascript
if (typeof document !== 'undefined') {
const style = document.createElement('style');
style.innerHTML = '.absdjfsdf__42__header {text-transform:uppercase;}';
document.head.appendChild(style);
}
```

You may want to use this option if your package does not have a bundler set up to handle CSS files.

`true` will use the default preprocessor, and you can set a custom preprocessor by passing an object with the value `preprocessor` set to your preprocessor.

### Variables

- `hash`: The hash of the content.
Expand Down Expand Up @@ -141,7 +159,7 @@ After that, your `package.json` should look like the following:
Now in your `preact.config.js`, we will modify the babel rule to use the necessary loaders and presets. Add the following:

```js
export default config => {
export default (config) => {
const { options, ...babelLoaderRule } = config.module.rules[0]; // Get the babel rule and options
options.presets.push('@babel/preset-react', 'linaria/babel'); // Push the necessary presets
config.module.rules[0] = {
Expand All @@ -150,15 +168,15 @@ export default config => {
use: [
{
loader: 'babel-loader',
options
options,
},
{
loader: 'linaria/loader',
options: {
babelOptions: options // Pass the current babel options to linaria's babel instance
}
}
]
babelOptions: options, // Pass the current babel options to linaria's babel instance
},
},
],
};
};
```
Expand Down Expand Up @@ -255,7 +273,7 @@ exports.onCreateWebpackConfig = ({ actions, loaders, getConfig, stage }) => {

config.module.rules = [
...config.module.rules.filter(
rule => String(rule.test) !== String(/\.js?$/)
(rule) => String(rule.test) !== String(/\.js?$/)
),

{
Expand Down
40 changes: 40 additions & 0 deletions src/__tests__/__snapshots__/babel.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Transpiles injecting the style tags into the document head if injectStyleTags is true 1`] = `
"import { css } from 'linaria';
import { styled } from 'linaria/react';
export const a = \\"ah6xni0\\";
export const B = /*#__PURE__*/styled(\\"div\\")({
name: \\"B\\",
class: \\"b1u0rrat\\"
});
export const C = /*#__PURE__*/styled(\\"div\\")({
name: \\"C\\",
class: \\"c1n8pbpy\\"
});

if (typeof document !== \\"undefined\\") {
const style = document.createElement(\\"style\\");
style.innerHTML = \\".ah6xni0{font-size:14px;}\\\\n.b1u0rrat{font-weight:bold;}\\\\n.c1n8pbpy .b1u0rrat{font-weight:normal;}\\\\n\\";
document.head.appendChild(style);
}"
`;

exports[`Transpiles injecting the style tags into the document head if injectStyleTags is true 2`] = `

CSS:

.ah6xni0 {
font-size: 14px;
}
.b1u0rrat {
font-weight: bold;
}
.c1n8pbpy {
.b1u0rrat {
font-weight: normal;
}
}

Dependencies: NA

`;

exports[`does not include styles if not referenced anywhere 1`] = `
"import { css } from 'linaria';
import { styled } from 'linaria/react';
Expand Down
27 changes: 27 additions & 0 deletions src/__tests__/babel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,30 @@ it('includes unreferenced styles for :global', async () => {
expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});

it('Transpiles injecting the style tags into the document head if injectStyleTags is true', async () => {
const { code, metadata } = await transpile(
dedent`
import { css } from 'linaria';
import { styled } from 'linaria/react';

export const a = css\`
font-size: 14px;
\`;

export const B = styled.div\`
font-weight: bold;
\`;

export const C = styled.div\`
${'${B}'} {
font-weight: normal;
}
\`;
`,
{ injectStyleTags: true }
);

expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});
46 changes: 46 additions & 0 deletions src/__tests__/preprocess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable no-template-curly-in-string */

import path from 'path';
import { transformUrl } from '../preprocess';

describe('transformUrl', () => {
type TransformUrlArgs = Parameters<typeof transformUrl>;
const dataset: Record<string, TransformUrlArgs> = {
'../assets/test.jpg': [
'./assets/test.jpg',
'./.linaria-cache/test.css',
'./test.js',
],
'../a/b/test.jpg': [
'../a/b/test.jpg',
'./.linaria-cache/test.css',
'./a/test.js',
],
};

it('should work with posix paths', () => {
for (const result of Object.keys(dataset)) {
expect(transformUrl(...dataset[result])).toBe(result);
}
});

it('should work with win32 paths', () => {
const toWin32 = (p: string) => p.split(path.posix.sep).join(path.win32.sep);
const win32Dataset = Object.keys(dataset).reduce(
(acc, key) => ({
...acc,
[key]: [
dataset[key][0],
toWin32(dataset[key][1]),
toWin32(dataset[key][2]),
path.win32,
] as TransformUrlArgs,
}),
{} as Record<string, TransformUrlArgs>
);

for (const result of Object.keys(win32Dataset)) {
expect(transformUrl(...win32Dataset[result])).toBe(result);
}
});
});
45 changes: 1 addition & 44 deletions src/__tests__/transform.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* eslint-disable no-template-curly-in-string */

import path from 'path';
import dedent from 'dedent';
import transform, { transformUrl } from '../transform';
import transform from '../transform';
import evaluator from '../babel/evaluators/extractor';

const outputFilename = './.linaria-cache/test.css';
Expand All @@ -14,48 +13,6 @@ const rules = [
},
];

describe('transformUrl', () => {
type TransformUrlArgs = Parameters<typeof transformUrl>;
const dataset: Record<string, TransformUrlArgs> = {
'../assets/test.jpg': [
'./assets/test.jpg',
'./.linaria-cache/test.css',
'./test.js',
],
'../a/b/test.jpg': [
'../a/b/test.jpg',
'./.linaria-cache/test.css',
'./a/test.js',
],
};

it('should work with posix paths', () => {
for (const result of Object.keys(dataset)) {
expect(transformUrl(...dataset[result])).toBe(result);
}
});

it('should work with win32 paths', () => {
const toWin32 = (p: string) => p.split(path.posix.sep).join(path.win32.sep);
const win32Dataset = Object.keys(dataset).reduce(
(acc, key) => ({
...acc,
[key]: [
dataset[key][0],
toWin32(dataset[key][1]),
toWin32(dataset[key][2]),
path.win32,
] as TransformUrlArgs,
}),
{} as Record<string, TransformUrlArgs>
);

for (const result of Object.keys(win32Dataset)) {
expect(transformUrl(...win32Dataset[result])).toBe(result);
}
});
});

it('rewrites a relative path in url() declarations', async () => {
const { cssText } = await transform(
dedent`
Expand Down
56 changes: 55 additions & 1 deletion src/babel/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { Node, Program, Expression } from '@babel/types';
import type { NodePath, Scope, Visitor } from '@babel/traverse';
import { expression, statement } from '@babel/template';
import generator from '@babel/generator';
import preprocess from '../preprocess';
import evaluate from './evaluators';
import getTemplateProcessor from './evaluators/templateProcessor';
import Module from './module';
Expand Down Expand Up @@ -104,6 +105,48 @@ function addLinariaPreval(
);
}

function injectStyleAst({ types: t }: Core, css: string) {
const createStyleElement = t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('style'),
t.callExpression(
t.memberExpression(
t.identifier('document'),
t.identifier('createElement')
),
[t.stringLiteral('style')]
)
),
]);

const assignInnerHTML = t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(t.identifier('style'), t.identifier('innerHTML')),
t.stringLiteral(css)
)
);

const appendToDOM = t.expressionStatement(
t.callExpression(
t.memberExpression(
t.memberExpression(t.identifier('document'), t.identifier('head')),
t.identifier('appendChild')
),
[t.identifier('style')]
)
);

return t.ifStatement(
t.binaryExpression(
'!==',
t.unaryExpression('typeof', t.identifier('document')),
t.stringLiteral('undefined')
),
t.blockStatement([createStyleElement, assignInnerHTML, appendToDOM])
);
}

export default function extract(
babel: Core,
options: StrictOptions
Expand Down Expand Up @@ -200,14 +243,25 @@ export default function extract(
);
state.queue.forEach((item) => process(item, state, valueCache));
},
exit(_: any, state: State) {
exit(path: NodePath<Program>, state: State) {
if (Object.keys(state.rules).length) {
// Store the result as the file metadata under linaria key
state.file.metadata.linaria = {
rules: state.rules,
replacements: state.replacements,
dependencies: state.dependencies,
};

if (options.injectStyleTags) {
const { cssText } = preprocess(state.rules, {
filename: state.file.opts.filename,
preprocessor:
options.injectStyleTags === true
? undefined
: options.injectStyleTags.preprocessor,
});
path.pushContainer('body', injectStyleAst(babel, cssText));
}
}

// Invalidate cache for module evaluation when we're done
Expand Down
2 changes: 2 additions & 0 deletions src/babel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { TransformOptions } from '@babel/core';
import type { NodePath } from '@babel/traverse';
import type { VisitorKeys } from '@babel/types';
import type { StyledMeta } from '../StyledMeta';
import { Preprocessor } from '../types';

export type JSONValue = string | number | boolean | JSONObject | JSONArray;

Expand Down Expand Up @@ -121,6 +122,7 @@ type ClassNameFn = (hash: string, title: string) => string;

export type StrictOptions = {
classNameSlug?: string | ClassNameFn;
injectStyleTags: { preprocessor: Preprocessor } | true | false;
displayName: boolean;
evaluate: boolean;
ignore?: RegExp;
Expand Down
1 change: 1 addition & 0 deletions src/babel/utils/loadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function loadOptions(
action: 'ignore',
},
],
injectStyleTags: false,
...(result ? result.config : null),
...rest,
};
Expand Down