summaryrefslogtreecommitdiff
path: root/@linaria/packages/extractor/src
diff options
context:
space:
mode:
Diffstat (limited to '@linaria/packages/extractor/src')
-rw-r--r--@linaria/packages/extractor/src/RequirementsResolver.ts212
-rw-r--r--@linaria/packages/extractor/src/index.ts144
2 files changed, 356 insertions, 0 deletions
diff --git a/@linaria/packages/extractor/src/RequirementsResolver.ts b/@linaria/packages/extractor/src/RequirementsResolver.ts
new file mode 100644
index 0000000..019177b
--- /dev/null
+++ b/@linaria/packages/extractor/src/RequirementsResolver.ts
@@ -0,0 +1,212 @@
+/**
+ * This file is used to extract statements required to evaluate dependencies.
+ * Starting from the exports.__linariaPreval passed as argument to static method on class RequirementsResolver,
+ * it recursively extracts paths that contains identifiers that are needed to evaluate the dependency.
+ */
+
+import { types as t } from '@babel/core';
+import type {
+ Identifier,
+ Node,
+ Statement,
+ VariableDeclarator,
+} from '@babel/types';
+import type { Binding, NodePath } from '@babel/traverse';
+
+type Requirement = {
+ result: Statement;
+ path: NodePath<Node>;
+ requirements: Set<NodePath>;
+};
+
+export default class RequirementsResolver {
+ public static resolve(path: NodePath<Node> | NodePath<Node>[]): Statement[] {
+ const resolver = new RequirementsResolver();
+ if (Array.isArray(path)) {
+ path.forEach((p) => this.resolve(p));
+ } else {
+ resolver.resolve(path);
+ }
+
+ return resolver.statements;
+ }
+
+ private requirements: Requirement[] = [];
+
+ /**
+ * Checks that specified node or one of its ancestors is already added
+ */
+ private isAdded(path: NodePath<Node>): boolean {
+ if (this.requirements.some((req) => req.path === path)) {
+ return true;
+ }
+
+ if (path.parentPath) {
+ return this.isAdded(path.parentPath);
+ }
+
+ return false;
+ }
+
+ /**
+ * Makes a declaration statement, finds dependencies
+ * and adds all of it to the list of requirements.
+ */
+ private resolveBinding(binding: Binding) {
+ let result: Statement;
+ const startPosition = binding.path.node.start;
+
+ switch (binding.kind) {
+ case 'module':
+ if (
+ binding.path.isImportSpecifier() &&
+ binding.path.parentPath.isImportDeclaration()
+ ) {
+ result = t.importDeclaration(
+ [binding.path.node],
+ binding.path.parentPath.node.source
+ );
+ } else {
+ result = binding.path.parentPath.node as Statement;
+ }
+ break;
+ case 'const':
+ case 'let':
+ case 'var': {
+ let decl = (binding.path as NodePath<VariableDeclarator>).node;
+ if (
+ binding.path.isVariableDeclarator() &&
+ t.isSequenceExpression(binding.path.node.init)
+ ) {
+ // Replace SequenceExpressions (expr1, expr2, expr3, ...) with the last one
+ decl = t.variableDeclarator(
+ binding.path.node.id,
+ binding.path.node.init.expressions[
+ binding.path.node.init.expressions.length - 1
+ ]
+ );
+ }
+
+ result = t.variableDeclaration(binding.kind, [decl]);
+ break;
+ }
+ default:
+ result = binding.path.node as Statement;
+ break;
+ }
+ // result may be newly created node that not have start/end/loc info
+ // which is needed to sort statements
+ result.start = startPosition;
+
+ const req: Requirement = {
+ result,
+ path: binding.path,
+ requirements: new Set(),
+ };
+
+ this.requirements.push(req);
+
+ req.requirements = this.resolve(binding.path);
+ }
+
+ /**
+ * Checks that a specified identifier has a binding and tries to resolve it
+ * @return `Binding` or null if there is no binding, or it is already added, or it has useless type
+ */
+ private resolveIdentifier(path: NodePath<Identifier>): Binding | null {
+ const binding = path.scope.getBinding(path.node.name);
+
+ if (
+ path.isReferenced() &&
+ binding &&
+ !this.isAdded(binding.path) &&
+ // @ts-ignore binding.kind can be param
+ binding.kind !== 'param'
+ ) {
+ this.resolveBinding(binding);
+ return binding;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds all identifiers in a specified path, finds all related bindings
+ * and recursively calls `resolve` for each of them.
+ * @return `Set` with related bindings
+ */
+ private resolve(path: NodePath<Node>): Set<NodePath> {
+ const set = new Set<NodePath>();
+ if (path.isIdentifier()) {
+ const binding = this.resolveIdentifier(path);
+ if (binding !== null) {
+ set.add(binding.path);
+ }
+
+ return set;
+ }
+
+ path.traverse({
+ Identifier: (p) => {
+ const binding = this.resolveIdentifier(p);
+ if (binding !== null) {
+ set.add(binding.path);
+ }
+ },
+ });
+
+ return set;
+ }
+
+ /**
+ * Returns sorted list of required statements
+ */
+ private get statements(): Statement[] {
+ const statements: Statement[] = [];
+ let requirements = this.requirements;
+ while (requirements.length > 0) {
+ // On each step, we add to the result list only that statements
+ // which don't have any dependencies (`zeroDeps`)
+ const [zeroDeps, rest] = requirements.reduce(
+ (acc, req) => {
+ if (req.requirements.size === 0) {
+ acc[0].push(req);
+ } else {
+ acc[1].push(req);
+ }
+
+ return acc;
+ },
+ [[], []] as [Requirement[], Requirement[]]
+ );
+
+ if (zeroDeps.length === 0) {
+ // That means that we are in the endless loop.
+ // I don't know how it's possible, but if it's ever happened, we at least would be notified.
+ throw new Error('Circular dependency');
+ }
+
+ statements.push(...zeroDeps.map((req) => req.result));
+ // Let's remove already added statements from the requirements of the rest of the list.
+ requirements = rest.map((req) => {
+ const reqs = new Set(req.requirements);
+ zeroDeps.forEach((r) => reqs.delete(r.path));
+ return {
+ ...req,
+ requirements: reqs,
+ };
+ });
+ }
+
+ // preserve original statements order, but reversed
+ statements.sort((a, b) => {
+ if (a.start && b.start) {
+ return b.start - a.start;
+ } else {
+ return 0;
+ }
+ });
+
+ return statements;
+ }
+}
diff --git a/@linaria/packages/extractor/src/index.ts b/@linaria/packages/extractor/src/index.ts
new file mode 100644
index 0000000..385b748
--- /dev/null
+++ b/@linaria/packages/extractor/src/index.ts
@@ -0,0 +1,144 @@
+/**
+ * This file is a main file of extractor evaluation strategy.
+ * It finds __linariaPreval statements starting from the end of the program and
+ * invoke RequirementsResolver to get parts of code that needs to be executed in order to evaluate the dependency.
+ */
+
+import { traverse, types as t } from '@babel/core';
+import type {
+ ExpressionStatement,
+ MemberExpression,
+ Program,
+ SequenceExpression,
+} from '@babel/types';
+import { parseSync, transformSync } from '@babel/core';
+import type { NodePath } from '@babel/traverse';
+import generator from '@babel/generator';
+
+import type { Evaluator } from '@linaria/babel-preset';
+import { buildOptions } from '@linaria/babel-preset';
+import RequirementsResolver from './RequirementsResolver';
+
+function isMemberExpression(
+ path: NodePath<any> | NodePath<any>[]
+): path is NodePath<MemberExpression> {
+ return !Array.isArray(path) && path.isMemberExpression();
+}
+
+// Checks that passed node is `exports.__linariaPreval = /* something */`
+function isLinariaPrevalExport(
+ path: NodePath<any>
+): path is NodePath<ExpressionStatement> {
+ if (!path.isExpressionStatement()) {
+ return false;
+ }
+
+ if (
+ !(path as NodePath<ExpressionStatement>)
+ .get('expression')
+ .isAssignmentExpression()
+ ) {
+ return false;
+ }
+
+ const left = path.get('expression.left');
+
+ if (!isMemberExpression(left)) {
+ return false;
+ }
+
+ const object = left.get('object');
+ const property = left.get('property');
+ if (
+ Array.isArray(property) ||
+ !property.isIdentifier() ||
+ property.node.name !== '__linariaPreval'
+ ) {
+ return false;
+ }
+
+ return object.isIdentifier() && object.node.name === 'exports';
+}
+
+const extractor: Evaluator = (filename, options, text, only = null) => {
+ const transformOptions = buildOptions(filename, options);
+ transformOptions.presets!.unshift([
+ require.resolve('@linaria/preeval'),
+ options,
+ ]);
+ transformOptions.plugins!.unshift([
+ require.resolve('@babel/plugin-transform-runtime'),
+ { useESModules: false },
+ ]);
+
+ // We made a mistake somewhen, and linaria preval was dependent on `plugin-transform-template-literals`
+ // Usually it was loaded into preval, because user was using `@babel/preset-env` preset which included that plugin. Internally we used this preset for tests (and previously for everything) - thats why we implemented behavior based on existing of that plugin
+ // The ordering is very important here, that's why it is added as a preset, not just as a plugin. It makes this plugin run *AFTER* linaria preset, which is required to make have the current behavior.
+ // In preval we have 2 visitors, one for Call Expressions and second for TaggedTemplateLiterals. Babel process TaggedTemplates first for some reason, and we grab only the css`` statements, we skip styled statements at this stage.
+ // Then it process TaggedTemplateLiterals with mentioned plugin, which transforms them to CallExpressions (babel seems to apply thw whole set of plugins for particular visitor, then for the next visitor and so on).
+ // Then Linaria can identify all `styled` as call expressions, including `styled.h1`, `styled.p` and others.
+
+ // Presets ordering is from last to first, so we add the plugin at the beginning of the list, which persist the order that was established with formerly used `@babel/preset-env`.
+
+ transformOptions.presets!.unshift({
+ plugins: [require.resolve('@babel/plugin-transform-template-literals')],
+ });
+ // Expressions will be extracted only for __linariaPreval.
+ // In all other cases a code will be returned as is.
+ let { code } = transformSync(text, transformOptions)!;
+ if (!only || only.length !== 1 || only[0] !== '__linariaPreval') {
+ return [code!, null];
+ }
+ // We cannot just use `ast` that was returned by `transformSync`,
+ // because there is some kind of cache inside `traverse` which
+ // reuses `NodePath` with a wrong scope.
+ // There is probably a better solution, but I haven't found it yet.
+ const ast = parseSync(code!, { filename: filename + '.preval' });
+ // First of all, let's find a __linariaPreval export
+ traverse(ast!, {
+ // We know that export has been added to the program body,
+ // so we don't need to traverse through the whole tree
+ Program(path: NodePath<Program>) {
+ const body = path.get('body');
+ // Highly likely it has been added in the end
+ for (let idx = body.length - 1; idx >= 0; idx--) {
+ if (isLinariaPrevalExport(body[idx])) {
+ // Here we are!
+ const statements = RequirementsResolver.resolve(
+ body[idx].get('expression.right')
+ );
+
+ // We only need to evaluate the last item in a sequence expression, e.g. (a, b, c)
+ body[idx].traverse({
+ SequenceExpression(sequence: NodePath<SequenceExpression>) {
+ sequence.replaceWith(
+ sequence.get('expressions')[
+ sequence.node.expressions.length - 1
+ ]
+ );
+ },
+ });
+
+ // We'll wrap each code in a block to avoid collisions in variable names
+ const wrapped = statements.reduce(
+ (acc, curr) => t.blockStatement([curr, acc]),
+ t.blockStatement([body[idx].node])
+ );
+
+ // Generate a new code with extracted statements
+ code = [
+ // Use String.raw to preserve escapes such as '\n' in the code
+ String.raw`${generator(wrapped).code}`,
+ ].join('\n');
+ break;
+ }
+ }
+
+ path.stop();
+ },
+ });
+
+ return [code!, null];
+};
+
+export default extractor;