Skip to content

Commit

Permalink
fix(shaker): support for "export * from …" (#809)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anber committed Jul 23, 2021
1 parent c0456c6 commit b06c1ba
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 7 deletions.
2 changes: 2 additions & 0 deletions packages/shaker/__fixtures__/bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const bar1 = 'bar1';
export const bar2 = 'bar2';
2 changes: 2 additions & 0 deletions packages/shaker/__fixtures__/foo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const foo1 = "foo1";
export const foo2 = "foo2";
2 changes: 2 additions & 0 deletions packages/shaker/__fixtures__/reexports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './foo';
export * from './bar';
18 changes: 18 additions & 0 deletions packages/shaker/__tests__/__snapshots__/shaker.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,24 @@ Dependencies: @babel/runtime/helpers/typeof, @linaria/babel-preset/__fixtures__/
`;
exports[`shaker should work with wildcard reexports 1`] = `
"import { css } from \\"@linaria/core\\";
import { foo1 } from \\"../__fixtures__/reexports\\";
export const square = \\"square_s1t92lw9\\";"
`;
exports[`shaker should work with wildcard reexports 2`] = `
CSS:
.square_s1t92lw9 {
color: foo1;
}
Dependencies: ../__fixtures__/reexports
`;
exports[`shaker simplifies react components 1`] = `
"import React from 'react';
import { styled } from '@linaria/react';
Expand Down
16 changes: 16 additions & 0 deletions packages/shaker/__tests__/shaker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ describe('shaker', () => {
expect(metadata).toMatchSnapshot();
});

it('should work with wildcard reexports', async () => {
const { code, metadata } = await transpile(
dedent`
import { css } from "@linaria/core";
import { foo1 } from "../__fixtures__/reexports";
export const square = css\`
color: ${'${foo1}'};
\`;
`
);

expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});

it('should interpolate imported components', async () => {
const { code, metadata } = await transpile(
dedent`
Expand Down
23 changes: 20 additions & 3 deletions packages/shaker/src/DepsGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@ function addEdge(this: DepsGraph, a: t.Node, b: t.Node) {
export default class DepsGraph {
public readonly imports: Map<string, t.Identifier[]> = new Map();
public readonly importAliases: Map<t.Identifier, string> = new Map();
public readonly importTypes: Map<string, 'wildcard' | 'default'> = new Map();
public readonly importTypes: Map<
string,
'wildcard' | 'default' | 'reexport'
> = new Map();
public readonly reexports: Array<t.Identifier> = [];

protected readonly parents: WeakMap<t.Node, t.Node> = new WeakMap();
protected readonly edges: Array<[t.Node, t.Node]> = [];
protected readonly exports: Map<string, t.Node> = new Map();
protected readonly dependencies: Map<t.Node, Set<t.Node>> = new Map();
Expand Down Expand Up @@ -74,6 +79,14 @@ export default class DepsGraph {
this.exports.set(name, node);
}

addParent(node: t.Node, parent: t.Node) {
this.parents.set(node, parent);
}

getParent(node: t.Node): t.Node | undefined {
return this.parents.get(node);
}

getDependenciesByBinding(id: string) {
this.processQueue();
const allReferences = this.getAllReferences(id);
Expand Down Expand Up @@ -122,10 +135,14 @@ export default class DepsGraph {
);
}

getLeafs(only: string[] | null): Array<t.Node | undefined> {
getLeaf(name: string): t.Node | undefined {
return this.exports.get(name);
}

getLeaves(only: string[] | null): Array<t.Node | undefined> {
this.processQueue();
return only
? only.map((name) => this.exports.get(name))
? only.map((name) => this.getLeaf(name))
: Array.from(this.exports.values());
}
}
4 changes: 4 additions & 0 deletions packages/shaker/src/graphBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ class GraphBuilder extends GraphBuilderState {
parentKey: VisitorKeys[TParent['type']] | null,
listIdx: number | null = null
): VisitorAction {
if (parent) {
this.graph.addParent(node, parent);
}

if (
this.isExportsAssigment(node) &&
!this.isExportsAssigment(node.right) &&
Expand Down
84 changes: 83 additions & 1 deletion packages/shaker/src/langs/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Block,
CallExpression,
Directive,
ExpressionStatement,
ForInStatement,
ForStatement,
Function,
Expand All @@ -26,6 +27,7 @@ import { peek } from '@linaria/babel-preset';
import type { IdentifierHandlers, Visitors } from '../types';
import GraphBuilderState from '../GraphBuilderState';
import ScopeManager from '../scope';
import DepsGraph from '../DepsGraph';

function isIdentifier(
node: Node,
Expand Down Expand Up @@ -73,6 +75,60 @@ function getCallee(node: CallExpression): Node {
return node.callee;
}

function findWildcardReexportStatement(
node: t.CallExpression,
identifierName: string,
graph: DepsGraph
): t.Statement | null {
if (!t.isIdentifier(node.callee) || node.callee.name !== 'require')
return null;

const declarator = graph.getParent(node);
if (!t.isVariableDeclarator(declarator)) return null;

const declaration = graph.getParent(declarator);
if (!t.isVariableDeclaration(declaration)) return null;

const program = graph.getParent(declaration);
if (!t.isProgram(program)) return null;

// Our node is a correct export
// Let's check that we have something that looks like transpiled re-export
return (
program.body.find((statement) => {
/*
* We are looking for `Object.keys(_bar).forEach(…)`
*/

if (!t.isExpressionStatement(statement)) return false;

const expression = statement.expression;
if (!t.isCallExpression(expression)) return false;

const callee = expression.callee;
if (!t.isMemberExpression(callee)) return false;

const { object, property } = callee;

if (!isIdentifier(property, 'forEach')) return false;

if (!t.isCallExpression(object)) return false;

// `object` should be `Object.keys`
if (
!t.isMemberExpression(object.callee) ||
!isIdentifier(object.callee.object, 'Object') ||
!isIdentifier(object.callee.property, 'keys')
)
return false;

//
const [argument] = object.arguments;
return isIdentifier(argument, identifierName);
}) ?? null
);
}

/*
* Returns nodes which are implicitly affected by specified node
*/
Expand Down Expand Up @@ -143,6 +199,16 @@ export const visitors: Visitors = {
}
},

/*
* ExpressionStatement
*/
ExpressionStatement(this: GraphBuilderState, node: ExpressionStatement) {
this.baseVisit(node);

this.graph.addEdge(node, node.expression);
this.graph.addEdge(node.expression, node);
},

/*
* BlockStatement | Program
* The same situation as in ExpressionStatement: if one of the expressions is required, the block itself is also required.
Expand Down Expand Up @@ -337,7 +403,8 @@ export const visitors: Visitors = {
const declaration = this.scope.getDeclaration(node.object);
if (
t.isIdentifier(declaration) &&
this.graph.importAliases.has(declaration)
this.graph.importAliases.has(declaration) &&
!node.computed
) {
// It is. We can remember what exactly we use from it.
const source = this.graph.importAliases.get(declaration)!;
Expand Down Expand Up @@ -486,6 +553,21 @@ export const visitors: Visitors = {
}
}

// Do we know the type of import?
if (!this.graph.importTypes.has(source)) {
// Is it a wildcard reexport? Let's check.
const statement = findWildcardReexportStatement(
node,
local.name,
this.graph
);
if (statement) {
this.graph.addEdge(local, statement);
this.graph.reexports.push(local);
this.graph.importTypes.set(source, 'reexport');
}
}

// The whole namespace was imported. We will know later, what exactly we need.
// eg. const slugify = require('../slugify');
this.graph.importAliases.set(local, source);
Expand Down
27 changes: 24 additions & 3 deletions packages/shaker/src/shaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,24 @@ export default function shake(

const depsGraph = build(rootPath);
const alive = new Set<Node>();
let deps: Node[] = depsGraph.getLeafs(exports).map((i) => i) as Node[];
const reexports: string[] = [];
let deps = (exports ?? [])
.map((token) => {
const node = depsGraph.getLeaf(token);
if (node) return [node];
// We have some unknown token. Do we have `export * from …` in that file?
if (depsGraph.reexports.length === 0) {
return [];
}

// If so, mark all re-exported files as required
reexports.push(token);
return [...depsGraph.reexports];
})
.reduce<Node[]>((acc, el) => {
acc.push(...el);
return acc;
}, []);
while (deps.length > 0) {
// Mark all dependencies as alive
deps.forEach((d) => alive.add(d));
Expand All @@ -88,12 +105,16 @@ export default function shake(

const imports = new Map<string, string[]>();
for (let [source, members] of depsGraph.imports.entries()) {
const defaultMembers =
depsGraph.importTypes.get(source) === 'wildcard' ? ['*'] : [];
const importType = depsGraph.importTypes.get(source);
const defaultMembers = importType === 'wildcard' ? ['*'] : [];
const aliveMembers = new Set(
members.filter((i) => alive.has(i)).map((i) => i.name)
);

if (importType === 'reexport') {
reexports.forEach((token) => aliveMembers.add(token));
}

imports.set(
source,
aliveMembers.size > 0 ? Array.from(aliveMembers) : defaultMembers
Expand Down

0 comments on commit b06c1ba

Please sign in to comment.