387 lines
9.7 KiB
JavaScript
387 lines
9.7 KiB
JavaScript
"use strict";
|
|
const {
|
|
parse,
|
|
types,
|
|
traverse,
|
|
loadOptions,
|
|
} = require("@babel/core");
|
|
const getTemplate = require("./get-template");
|
|
const loadSyntax = require("postcss-syntax/load-syntax");
|
|
|
|
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 '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;
|
|
}
|
|
// because some options need to be passed to parser also
|
|
opts[key] = fileOpts[key];
|
|
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();
|
|
let objLiteral = new Set();
|
|
let 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;
|
|
}
|
|
}
|
|
|
|
function setSpecifier (id, nameSpace) {
|
|
nameSpace.unshift.apply(
|
|
nameSpace,
|
|
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.apply(nameSpace, 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) {
|
|
let result = name && ((supports.hasOwnProperty(name) && supports[name]) || (opts.syntax.config.hasOwnProperty(name) && opts.syntax.config[name]));
|
|
switch (typeof result) {
|
|
case "function": {
|
|
result = result.apply(this, Array.prototype.slice.call(arguments, 1));
|
|
}
|
|
// eslint-disable-next-line no-fallthrough
|
|
case "boolean": {
|
|
return result;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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());
|
|
|
|
objLiteral = 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",
|
|
};
|
|
});
|
|
|
|
tplLiteral = Array.from(tplLiteral).filter(node => (
|
|
objLiteral.every(style => (
|
|
node.start > style.endIndex || node.end < style.startIndex
|
|
))
|
|
)).map(node => {
|
|
const quasis = node.quasis.map(node => ({
|
|
start: node.start,
|
|
end: node.end,
|
|
}));
|
|
const style = {
|
|
startIndex: quasis[0].start,
|
|
endIndex: quasis[quasis.length - 1].end,
|
|
content: getTemplate(node, source),
|
|
};
|
|
if (node.expressions.length) {
|
|
style.syntax = loadSyntax(opts, __dirname);
|
|
style.lang = "template-literal";
|
|
style.opts = {
|
|
quasis: quasis,
|
|
};
|
|
} else {
|
|
style.lang = "css";
|
|
}
|
|
return style;
|
|
});
|
|
|
|
return (styles || []).concat(objLiteral).concat(tplLiteral);
|
|
};
|
|
|
|
module.exports = literalParser;
|