saepequia
Version:
A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps
200 lines (172 loc) • 5.76 kB
text/typescript
/**
* This file exposes transform function that:
* - parse the passed code to AST
* - transforms the AST using Linaria babel preset ('./babel/index.js) and additional config defined in Linaria config file or passed to bundler configuration.
* - runs generated CSS files through default of user-defined preprocessor
* - generates source maps for CSS files
* - return transformed code (without Linaria template literals), generated CSS, source maps and babel metadata from transform step.
*/
import path from 'path';
import { parseSync, transformFromAstSync } from '@babel/core';
import stylis from 'stylis';
import type { Mapping } from 'source-map';
import { SourceMapGenerator } from 'source-map';
import loadOptions from './babel/utils/loadOptions';
import { debug } from './babel/utils/logger';
import type { LinariaMetadata, Options, PreprocessorFn, Result } from './types';
const STYLIS_DECLARATION = 1;
const posixSep = path.posix.sep;
const babelPreset = require.resolve('./babel');
export function transformUrl(
url: string,
outputFilename: string,
sourceFilename: string,
platformPath: typeof path = path
) {
// Replace asset path with new path relative to the output CSS
const relative = platformPath.relative(
platformPath.dirname(outputFilename),
// Get the absolute path to the asset from the path relative to the JS file
platformPath.resolve(platformPath.dirname(sourceFilename), url)
);
if (platformPath.sep === posixSep) {
return relative;
}
return relative.split(platformPath.sep).join(posixSep);
}
export function shouldTransformCode(code: string): boolean {
return /\b(styled|css)/.test(code);
}
export function extractCssFromAst(
babelFileResult: babel.BabelFileResult,
code: string,
options: Options
) {
const { metadata, code: transformedCode, map } = babelFileResult;
if (
!metadata ||
!(metadata as babel.BabelFileMetadata & { linaria: LinariaMetadata })
.linaria
) {
return {
code: transformedCode || '', // if there was only unused code we want to return transformed code which will be later removed by the bundler
sourceMap: map
? {
...map,
version: map.version.toString(),
}
: null,
};
}
const {
rules,
replacements,
dependencies,
} = (metadata as babel.BabelFileMetadata & {
linaria: LinariaMetadata;
}).linaria;
const mappings: Mapping[] = [];
let cssText = '';
let preprocessor: PreprocessorFn;
if (typeof options.preprocessor === 'function') {
// eslint-disable-next-line prefer-destructuring
preprocessor = options.preprocessor;
} else {
switch (options.preprocessor) {
case 'none':
preprocessor = (selector, text) => `${selector} {${text}}\n`;
break;
case 'stylis':
default:
stylis.use(null)((context, decl) => {
const { outputFilename } = options;
if (context === STYLIS_DECLARATION && outputFilename) {
// When writing to a file, we need to adjust the relative paths inside url(..) expressions
// It'll allow css-loader to resolve an imported asset properly
return decl.replace(
/\b(url\((["']?))(\.[^)]+?)(\2\))/g,
(match, p1, p2, p3, p4) =>
p1 + transformUrl(p3, outputFilename, options.filename) + p4
);
}
return decl;
});
preprocessor = stylis;
}
}
Object.keys(rules).forEach((selector, index) => {
mappings.push({
generated: {
line: index + 1,
column: 0,
},
original: rules[selector].start!,
name: selector,
source: '',
});
// Run each rule through stylis to support nesting
cssText += `${preprocessor(selector, rules[selector].cssText)}\n`;
});
return {
code: transformedCode || '',
cssText,
rules,
replacements,
dependencies,
sourceMap: map
? {
...map,
version: map.version.toString(),
}
: null,
get cssSourceMapText() {
if (mappings?.length) {
const generator = new SourceMapGenerator({
file: options.filename.replace(/\.js$/, '.css'),
});
mappings.forEach((mapping) =>
generator.addMapping(
Object.assign({}, mapping, { source: options.filename })
)
);
generator.setSourceContent(options.filename, code);
return generator.toString();
}
return '';
},
};
}
export default function transform(code: string, options: Options): Result {
// Check if the file contains `css` or `styled` words first
// Otherwise we should skip transforming
if (!shouldTransformCode(code)) {
return {
code,
sourceMap: options.inputSourceMap,
};
}
debug(
'transform',
`${options.filename} to ${options.outputFilename}\n${code}`
);
const pluginOptions = loadOptions(options.pluginOptions);
const babelOptions = pluginOptions?.babelOptions ?? null;
// Parse the code first so babel uses user's babel config for parsing
// We don't want to use user's config when transforming the code
const ast = parseSync(code, {
...babelOptions,
filename: options.filename,
caller: { name: 'linaria' },
});
const babelFileResult = transformFromAstSync(ast!, code, {
...(babelOptions?.rootMode ? { rootMode: babelOptions.rootMode } : null),
filename: options.filename,
presets: [[babelPreset, pluginOptions]],
babelrc: false,
configFile: false,
sourceMaps: true,
sourceFileName: options.filename,
inputSourceMap: options.inputSourceMap,
})!;
return extractCssFromAst(babelFileResult, code, options);
}