You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
488 lines
12 KiB
488 lines
12 KiB
'use strict'; |
|
|
|
const getTemplate = require('./get-template'); |
|
const loadSyntax = require('postcss-syntax/load-syntax'); |
|
const { parse, types, traverse, loadOptions } = require('@babel/core'); |
|
|
|
const isStyleSheetCreate = expectAdjacentSibling(['create']); |
|
const supports = { |
|
// import styled from '@emotion/styled' |
|
// import { styled } from 'glamor/styled' |
|
// import { styled } from "styletron-react"; |
|
// import { styled } from 'linaria/react'; |
|
// import { styled } from '@material-ui/styles' |
|
styled: true, |
|
|
|
// import { style } from "typestyle"; |
|
style: true, |
|
|
|
// import { StyleSheet, css } from 'aphrodite'; |
|
// import styled, { css } from 'astroturf'; |
|
// import { css } from 'lit-css'; |
|
// import { css } from 'glamor' |
|
// require('css-light').css({color: 'red'}); |
|
// import { css } from 'linaria'; |
|
css: true, |
|
|
|
// import { StyleSheet, css } from 'aphrodite'; |
|
// import { AppRegistry, StyleSheet, Text, View } from 'react-native'; |
|
StyleSheet: isStyleSheetCreate, |
|
|
|
// import styled, { css } from 'astroturf'; |
|
astroturf: true, |
|
|
|
// require('csjs')`css`; |
|
csjs: true, |
|
|
|
// require('cssobj')({color: 'red'}) |
|
cssobj: true, |
|
|
|
// require('electron-css')({color: 'red'}) |
|
'electron-css': true, |
|
|
|
// import styled from "react-emotion"; |
|
'react-emotion': true, |
|
|
|
// import styled from 'vue-emotion'; |
|
// Also see: |
|
// - https://github.com/stylelint/stylelint/issues/4247 |
|
// - https://github.com/gucong3000/postcss-jsx/issues/63 |
|
// - https://github.com/stylelint/postcss-css-in-js/issues/22 |
|
'vue-emotion': true, |
|
|
|
// import styled from 'preact-emotion' |
|
'preact-emotion': true, |
|
|
|
// https://github.com/streamich/freestyler |
|
freestyler: true, |
|
|
|
// https://github.com/paypal/glamorous |
|
glamorous: true, |
|
|
|
// https://github.com/irom-io/i-css |
|
// "i-css": (i, nameSpace) => nameSpace[i + 1] === "addStyles" && nameSpace[i + 2] === "wrapper", |
|
|
|
// https://github.com/j2css/j2c |
|
j2c: expectAdjacentSibling(['inline', 'sheet']), |
|
|
|
// var styles = StyleSheet.create({color: 'red'}) |
|
'react-inline': isStyleSheetCreate, |
|
'react-style': isStyleSheetCreate, |
|
|
|
// import reactCSS from 'reactcss' |
|
reactcss: true, |
|
|
|
// const StyledButton = injectSheet(styles)(Button) |
|
'react-jss': true, |
|
|
|
// import styled from 'styled-components'; |
|
'styled-components': true, |
|
|
|
// import {withStyle} from "styletron-react"; |
|
'styletron-react': expectAdjacentSibling(['withStyle']), |
|
|
|
styling: true, |
|
|
|
// const rule = superstyle({ color: 'blue' }) |
|
superstyle: true, |
|
|
|
// import { makeStyles } from '@material-ui/styles' |
|
styles: expectAdjacentSibling(['makeStyles']), |
|
}; |
|
|
|
const plugins = [ |
|
'jsx', |
|
'typescript', |
|
'objectRestSpread', |
|
['decorators', { decoratorsBeforeExport: false }], |
|
'classProperties', |
|
'exportExtensions', |
|
'asyncGenerators', |
|
'functionBind', |
|
'functionSent', |
|
'dynamicImport', |
|
'optionalCatchBinding', |
|
]; |
|
|
|
function expectAdjacentSibling(names) { |
|
return (i, nameSpace) => names.some((name) => nameSpace[i + 1] === name); |
|
} |
|
|
|
function loadBabelOpts(opts) { |
|
const filename = opts.from && opts.from.replace(/\?.*$/, ''); |
|
|
|
opts = { |
|
filename, |
|
parserOpts: { |
|
plugins, |
|
sourceFilename: filename, |
|
sourceType: filename && /\.m[tj]sx?$/.test(filename) ? 'module' : 'unambiguous', |
|
allowImportExportEverywhere: true, |
|
allowAwaitOutsideFunction: true, |
|
allowReturnOutsideFunction: true, |
|
allowSuperOutsideMethod: true, |
|
}, |
|
}; |
|
let fileOpts; |
|
|
|
try { |
|
fileOpts = |
|
filename && |
|
loadOptions({ |
|
filename, |
|
}); |
|
} catch (ex) { |
|
// |
|
} |
|
|
|
for (const key in fileOpts) { |
|
if (Array.isArray(fileOpts[key]) && !fileOpts[key].length) { |
|
continue; |
|
} |
|
|
|
opts[key] = fileOpts[key]; |
|
|
|
if (Array.isArray(fileOpts[key]) && Array.isArray(opts.parserOpts[key])) { |
|
// combine arrays for plugins |
|
// plugins in fileOpts could be string, array or object |
|
for (const plugin of fileOpts[key]) { |
|
const option = |
|
Array.isArray(plugin) || typeof plugin === 'string' |
|
? plugin |
|
: [plugin.key, plugin.options]; |
|
|
|
opts.parserOpts[key] = [...opts.parserOpts[key], option]; |
|
} |
|
} else { |
|
// because some options need to be passed to parser also |
|
opts.parserOpts[key] = fileOpts[key]; |
|
} |
|
} |
|
|
|
return opts; |
|
} |
|
|
|
function literalParser(source, opts, styles) { |
|
let ast; |
|
|
|
try { |
|
ast = parse(source, loadBabelOpts(opts)); |
|
} catch (ex) { |
|
// console.error(ex); |
|
return styles || []; |
|
} |
|
|
|
const specifiers = new Map(); |
|
const variableDeclarator = new Map(); |
|
const objLiteral = new Set(); |
|
const tplLiteral = new Set(); |
|
const tplCallee = new Set(); |
|
const jobs = []; |
|
|
|
function addObjectJob(path) { |
|
jobs.push(() => { |
|
addObjectValue(path); |
|
}); |
|
} |
|
|
|
function addObjectValue(path) { |
|
if (path.isIdentifier()) { |
|
const identifier = path.scope.getBindingIdentifier(path.node.name); |
|
|
|
if (identifier) { |
|
path = variableDeclarator.get(identifier); |
|
|
|
if (path) { |
|
variableDeclarator.delete(identifier); |
|
path.forEach(addObjectExpression); |
|
} |
|
} |
|
} else { |
|
addObjectExpression(path); |
|
} |
|
} |
|
|
|
function addObjectExpression(path) { |
|
if (path.isObjectExpression()) { |
|
path.get('properties').forEach((prop) => { |
|
if (prop.isSpreadElement()) { |
|
addObjectValue(prop.get('argument')); |
|
} |
|
}); |
|
objLiteral.add(path.node); |
|
|
|
return path; |
|
} |
|
|
|
// If this is not an object but a function returning an object, we want to parse the |
|
// object that is in the body of the function. We will only parse it if the body only |
|
// consist of an object and nothing else. |
|
if (path.isArrowFunctionExpression()) { |
|
const body = path.get('body'); |
|
|
|
if (body) { |
|
addObjectExpression(body); |
|
} |
|
} |
|
} |
|
|
|
function setSpecifier(id, nameSpace) { |
|
nameSpace.unshift( |
|
...nameSpace |
|
.shift() |
|
.replace(/^\W+/, '') |
|
.split(/[/\\]+/g), |
|
); |
|
|
|
if (types.isIdentifier(id)) { |
|
specifiers.set(id.name, nameSpace); |
|
specifiers.set(id, nameSpace); |
|
} else if (types.isObjectPattern(id)) { |
|
id.properties.forEach((property) => { |
|
if (types.isObjectProperty(property)) { |
|
const key = property.key; |
|
|
|
nameSpace = nameSpace.concat(key.name || key.value); |
|
id = property.value; |
|
} else { |
|
id = property.argument; |
|
} |
|
|
|
setSpecifier(id, nameSpace); |
|
}); |
|
} else if (types.isArrayPattern(id)) { |
|
id.elements.forEach((element, i) => { |
|
setSpecifier(element, nameSpace.concat(String(i))); |
|
}); |
|
} |
|
} |
|
|
|
function getNameSpace(path, nameSpace) { |
|
let node = path.node; |
|
|
|
if (path.isIdentifier() || path.isJSXIdentifier()) { |
|
node = path.scope.getBindingIdentifier(node.name) || node; |
|
const specifier = specifiers.get(node) || specifiers.get(node.name); |
|
|
|
if (specifier) { |
|
nameSpace.unshift(...specifier); |
|
} else { |
|
nameSpace.unshift(node.name); |
|
} |
|
} else { |
|
['name', 'property', 'object', 'callee'].forEach((prop) => { |
|
node[prop] && getNameSpace(path.get(prop), nameSpace); |
|
}); |
|
} |
|
|
|
return nameSpace; |
|
} |
|
|
|
function isStylePath(path) { |
|
return getNameSpace(path, []).some(function (name, ...args) { |
|
const result = |
|
name && |
|
((Object.prototype.hasOwnProperty.call(supports, name) && supports[name]) || |
|
(Object.prototype.hasOwnProperty.call(opts.syntax.config, name) && |
|
opts.syntax.config[name])); |
|
|
|
switch (typeof result) { |
|
case 'function': { |
|
return result.apply(this, args); |
|
} |
|
case 'boolean': { |
|
return result; |
|
} |
|
default: { |
|
return undefined; |
|
} |
|
} |
|
}); |
|
} |
|
|
|
const visitor = { |
|
ImportDeclaration: (path) => { |
|
const moduleId = path.node.source.value; |
|
|
|
path.node.specifiers.forEach((specifier) => { |
|
const nameSpace = [moduleId]; |
|
|
|
if (specifier.imported) { |
|
nameSpace.push(specifier.imported.name); |
|
} |
|
|
|
setSpecifier(specifier.local, nameSpace); |
|
}); |
|
}, |
|
JSXAttribute: (path) => { |
|
if (/^(?:css|style)$/.test(path.node.name.name)) { |
|
addObjectJob(path.get('value.expression')); |
|
} |
|
}, |
|
VariableDeclarator: (path) => { |
|
variableDeclarator.set(path.node.id, path.node.init ? [path.get('init')] : []); |
|
}, |
|
AssignmentExpression: (path) => { |
|
if (types.isIdentifier(path.node.left) && types.isObjectExpression(path.node.right)) { |
|
const identifier = path.scope.getBindingIdentifier(path.node.left.name); |
|
const variable = variableDeclarator.get(identifier); |
|
const valuePath = path.get('right'); |
|
|
|
if (variable) { |
|
variable.push(valuePath); |
|
} else { |
|
variableDeclarator.set(identifier, [valuePath]); |
|
} |
|
} |
|
}, |
|
CallExpression: (path) => { |
|
const callee = path.node.callee; |
|
|
|
if ( |
|
types.isIdentifier(callee, { name: 'require' }) && |
|
!path.scope.getBindingIdentifier(callee.name) |
|
) { |
|
path.node.arguments.filter(types.isStringLiteral).forEach((arg) => { |
|
const moduleId = arg.value; |
|
const nameSpace = [moduleId]; |
|
let currPath = path; |
|
|
|
do { |
|
let id = currPath.parent.id; |
|
|
|
if (!id) { |
|
id = currPath.parent.left; |
|
|
|
if (id) { |
|
id = path.scope.getBindingIdentifier(id.name) || id; |
|
} else { |
|
if (types.isIdentifier(currPath.parent.property)) { |
|
nameSpace.push(currPath.parent.property.name); |
|
} |
|
|
|
currPath = currPath.parentPath; |
|
continue; |
|
} |
|
} |
|
|
|
setSpecifier(id, nameSpace); |
|
break; |
|
} while (currPath); |
|
}); |
|
} else if (!tplCallee.has(callee) && isStylePath(path.get('callee'))) { |
|
path.get('arguments').forEach((arg) => { |
|
addObjectJob(arg.isFunction() ? arg.get('body') : arg); |
|
}); |
|
} |
|
}, |
|
TaggedTemplateExpression: (path) => { |
|
if (isStylePath(path.get('tag'))) { |
|
tplLiteral.add(path.node.quasi); |
|
|
|
if (path.node.tag.callee) { |
|
tplCallee.add(path.node.tag.callee); |
|
} |
|
} |
|
}, |
|
}; |
|
|
|
traverse(ast, visitor); |
|
jobs.forEach((job) => job()); |
|
|
|
const objLiteralStyles = Array.from(objLiteral).map((endNode) => { |
|
const objectSyntax = require('./object-syntax'); |
|
let startNode = endNode; |
|
|
|
if (startNode.leadingComments && startNode.leadingComments.length) { |
|
startNode = startNode.leadingComments[0]; |
|
} |
|
|
|
let startIndex = startNode.start; |
|
const before = source.slice(startNode.start - startNode.loc.start.column, startNode.start); |
|
|
|
if (/^\s+$/.test(before)) { |
|
startIndex -= before.length; |
|
} |
|
|
|
return { |
|
startIndex, |
|
endIndex: endNode.end, |
|
skipConvert: true, |
|
content: source, |
|
opts: { |
|
node: endNode, |
|
}, |
|
syntax: objectSyntax, |
|
lang: 'object-literal', |
|
}; |
|
}); |
|
|
|
const tplLiteralStyles = []; |
|
|
|
Array.from(tplLiteral).forEach((node) => { |
|
if ( |
|
objLiteralStyles.some((style) => style.startIndex <= node.end && node.start < style.endIndex) |
|
) { |
|
return; |
|
} |
|
|
|
const quasis = node.quasis.map((quasiNode) => ({ |
|
start: quasiNode.start, |
|
end: quasiNode.end, |
|
})); |
|
const style = { |
|
startIndex: quasis[0].start, |
|
endIndex: quasis[quasis.length - 1].end, |
|
content: getTemplate(node, source), |
|
}; |
|
|
|
if (node.expressions.length) { |
|
const expressions = node.expressions.map((expressionNode) => ({ |
|
start: expressionNode.start, |
|
end: expressionNode.end, |
|
})); |
|
|
|
style.syntax = loadSyntax(opts, __dirname); |
|
style.lang = 'template-literal'; |
|
style.opts = { |
|
quasis, |
|
expressions, |
|
}; |
|
} else { |
|
style.lang = 'css'; |
|
} |
|
|
|
let parent = null; |
|
let targetStyles = tplLiteralStyles; |
|
|
|
while (targetStyles) { |
|
const target = targetStyles.find( |
|
(targetStyle) => |
|
targetStyle.opts && |
|
targetStyle.opts.expressions.some( |
|
(expr) => expr.start <= style.startIndex && style.endIndex < expr.end, |
|
), |
|
); |
|
|
|
if (target) { |
|
parent = target; |
|
targetStyles = target.opts.templateLiteralStyles; |
|
} else { |
|
break; |
|
} |
|
} |
|
|
|
if (parent) { |
|
const templateLiteralStyles = |
|
parent.opts.templateLiteralStyles || (parent.opts.templateLiteralStyles = []); |
|
|
|
templateLiteralStyles.push(style); |
|
} else { |
|
tplLiteralStyles.push(style); |
|
} |
|
}); |
|
|
|
return (styles || []).concat(objLiteralStyles).concat(tplLiteralStyles); |
|
} |
|
|
|
module.exports = literalParser;
|
|
|