Skip to content
This repository has been archived by the owner on Sep 4, 2023. It is now read-only.

Commit

Permalink
refactor: ExtractOptions -> tag/fn/prop matchers
Browse files Browse the repository at this point in the history
feat: expose findIdentifierValueDeclaration, getDeclarationFor, isScope

opti: forEachDescendant directly on extractable nodes rather than identifier
  • Loading branch information
astahmer committed Mar 1, 2023
1 parent 484db8c commit cec6e1e
Show file tree
Hide file tree
Showing 30 changed files with 516 additions and 605 deletions.
7 changes: 7 additions & 0 deletions .changeset/great-ravens-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@box-extractor/core": minor
---

refactor(core): match tag/fn/prop functions for better control on what gets extracted

opti: forEachDescendant directly on extractable nodes rather than identifier
24 changes: 2 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,8 @@ if you need the static analysis (using [ts-morph](https://github.com/dsherret/ts
pnpm add @box-extractor/core
```

### core/vite

there are 2 plugins from `@box-extractor/core` :

- `createViteBoxExtractor` that will statically analyze your TS(X) files & extract functions args / JSX component props values
- `createViteBoxRefUsageFinder` will statically analyze your TS(X) files & recursively find every transitive components (the one being spread onto) used from a list of root components

```ts
import { createViteBoxExtractor, createViteBoxRefUsageFinder } from "@box-extractor/core";
```

### core/esbuild

only the `createEsbuildBoxExtractor` is made/exported atm from `@box-extractor/core`, it does the same as its vite counterpart
or you could try it like this:

```ts
import { createEsbuildBoxExtractor } from "@box-extractor/core";
pnpx @box-extractor/cli -i path/to/input.ts -o path/to/report.json --functions="css,styled" --components="div,factory.*,SomeComponent"
```

## TODO

some things that are missing:

- tsup/esbuild example (the esbuild plugins are ready today: `createEsbuildBoxExtractor`)
- [maybe TODO - finder - find all wrapping functions recursively, just like components](https://github.com/astahmer/box-extractor/issues/13)
323 changes: 157 additions & 166 deletions packages/box-extractor/src/extractor/extract.ts

Large diffs are not rendered by default.

45 changes: 34 additions & 11 deletions packages/box-extractor/src/extractor/extractAtRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,37 @@ import { createLogger } from "@box-extractor/logger";
import { JsxOpeningElement, JsxSelfClosingElement, Node, SourceFile, ts } from "ts-morph";

import { extractCallExpressionValues } from "./extractCallExpressionValues";
import { extractJsxAttributeIdentifierValue } from "./extractJsxAttributeIdentifierValue";
import { extractJsxAttribute } from "./extractJsxAttributeIdentifierValue";
import { extractJsxSpreadAttributeValues } from "./extractJsxSpreadAttributeValues";
import { box, BoxNode } from "./type-factory";
import type { ComponentMatchers, FunctionMatchers } from "./types";

const logger = createLogger("box-ex:extractor:extractAtRange");

export const extractAtRange = (source: SourceFile, line: number, column: number) => {
export const extractAtRange = (
source: SourceFile,
line: number,
column: number,
matchProp: ComponentMatchers["matchProp"] | FunctionMatchers["matchProp"] = () => true
) => {
const node = getTsNodeAtPosition(source, line, column);
logger({ line, column, node: node?.getKindName() });
if (!node) return;

// pointing directly at the node
if (Node.isJsxOpeningElement(node) || Node.isJsxSelfClosingElement(node)) {
return extractJsxElementProps(node);
return extractJsxElementProps(node, matchProp as ComponentMatchers["matchProp"]);
}

if (Node.isCallExpression(node)) {
// TODO box.function(node) ?
return extractCallExpressionValues(node, "all");
return extractCallExpressionValues(node, (prop) =>
(matchProp as FunctionMatchers["matchProp"])({
...prop,
fnNode: node,
fnName: node.getExpression().getText(),
})
);
}

// pointing at the name
Expand All @@ -30,24 +42,33 @@ export const extractAtRange = (source: SourceFile, line: number, column: number)
logger({ line, column, parent: parent?.getKindName() });

if (Node.isJsxOpeningElement(parent) || Node.isJsxSelfClosingElement(parent)) {
return extractJsxElementProps(parent);
return extractJsxElementProps(parent, matchProp as ComponentMatchers["matchProp"]);
}

if (Node.isPropertyAccessExpression(parent)) {
const grandParent = parent.getParent();
if (Node.isJsxOpeningElement(grandParent) || Node.isJsxSelfClosingElement(grandParent)) {
return extractJsxElementProps(grandParent);
return extractJsxElementProps(grandParent, matchProp as ComponentMatchers["matchProp"]);
}
}

if (Node.isCallExpression(parent)) {
// TODO box.function(node) ?
return extractCallExpressionValues(parent, "all");
return extractCallExpressionValues(parent, (prop) =>
(matchProp as FunctionMatchers["matchProp"])({
...prop,
fnNode: parent,
fnName: parent.getExpression().getText(),
})
);
}
}
};

export const extractJsxElementProps = (node: JsxOpeningElement | JsxSelfClosingElement) => {
export const extractJsxElementProps = (
node: JsxOpeningElement | JsxSelfClosingElement,
matchProp: ComponentMatchers["matchProp"]
) => {
const tagName = node.getTagNameNode().getText();
const jsxAttributes = node.getAttributes();
logger.scoped("jsx", { tagName, jsxAttributes: jsxAttributes.length });
Expand All @@ -56,8 +77,7 @@ export const extractJsxElementProps = (node: JsxOpeningElement | JsxSelfClosingE
jsxAttributes.forEach((attrNode) => {
if (Node.isJsxAttribute(attrNode)) {
const nameNode = attrNode.getNameNode();
const maybeValue =
extractJsxAttributeIdentifierValue(attrNode.getNameNode()) ?? box.unresolvable(nameNode, []);
const maybeValue = extractJsxAttribute(attrNode) ?? box.unresolvable(nameNode, []);
props.set(nameNode.getText(), maybeValue);
return;
}
Expand All @@ -70,7 +90,10 @@ export const extractJsxElementProps = (node: JsxOpeningElement | JsxSelfClosingE
const getSpreadPropName = () => `_SPREAD_${propSizeAtThisPoint}_${count++}`;

const spreadPropName = getSpreadPropName();
const maybeValue = extractJsxSpreadAttributeValues(attrNode, "all") ?? box.unresolvable(attrNode, []);
const maybeValue =
extractJsxSpreadAttributeValues(attrNode, (prop) =>
matchProp({ ...prop, tagName, tagNode: node } as any)
) ?? box.unresolvable(attrNode, []);
props.set(spreadPropName, maybeValue);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { maybeBoxNode } from "./maybeBoxNode";

import { maybeObjectLikeBox } from "./maybeObjectLikeBox";
import { box } from "./type-factory";
import type { ListOrAll } from "./types";
import type { MatchFnPropArgs } from "./types";
import { isNotNullish, unwrapExpression } from "./utils";

const logger = createLogger("box-ex:extractor:call-expr");

export const extractCallExpressionValues = (node: CallExpression, properties: ListOrAll) => {
export const extractCallExpressionValues = (node: CallExpression, matchProp: (prop: MatchFnPropArgs) => boolean) => {
const argList = node.getArguments();
if (argList.length === 0) return box.list([], node, []);

Expand All @@ -22,18 +22,17 @@ export const extractCallExpressionValues = (node: CallExpression, properties: Li

const maybeValue = maybeBoxNode(argNode, stack);
logger({ extractCallExpression: true, maybeValue });
// !maybeValue && console.log("maybeBoxNode empty", expression.getKindName(), expression.getText());
if (maybeValue) {
return maybeValue;
}

const maybeObject = maybeObjectLikeBox(argNode, stack, properties);
const maybeObject = maybeObjectLikeBox(argNode, stack, matchProp);
logger({ maybeObject });
// console.log("expr", expression.getKindName(), expression.getText());
if (maybeObject) return maybeObject;
})
.filter(isNotNullish);

// TODO box.function
if (boxes.length === 0) return;
if (boxes.length === 1) return boxes[0]!;

Expand Down
5 changes: 4 additions & 1 deletion packages/box-extractor/src/extractor/extractFunctionFrom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export const extractFunctionFrom = <Result>(
} = {}
) => {
const resultByName = new Map<string, { result: Result; queryBox: BoxNodeList; nameNode: () => BindingName }>();
const extractedTheme = extract({ ast: sourceFile, functions: [functionName] });
const extractedTheme = extract({
ast: sourceFile,
functions: { matchFn: ({ fnName }) => fnName === functionName, matchProp: () => true },
});
const fnExtraction = extractedTheme.get(functionName) as ExtractedFunctionResult;
if (!fnExtraction) return resultByName;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Identifier } from "ts-morph";
import { Node } from "ts-morph";
import { createLogger } from "@box-extractor/logger";
import type { JsxAttribute } from "ts-morph";
import { Node } from "ts-morph";

import { maybeBoxNode } from "./maybeBoxNode";
import { maybeObjectLikeBox } from "./maybeObjectLikeBox";
Expand All @@ -9,18 +9,16 @@ import { unwrapExpression } from "./utils";

const logger = createLogger("box-ex:extractor:jsx-attr");

export const extractJsxAttributeIdentifierValue = (identifier: Identifier) => {
// console.log(n.getText(), n.parent.getText());
const parent = identifier.getParent();
if (!Node.isJsxAttribute(parent)) return;
// TODO rename file to extractJsxAttribute
export const extractJsxAttribute = (jsxAttribute: JsxAttribute) => {
// <ColorBox color="red.200" backgroundColor="blackAlpha.100" />
// ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// identifier = `color` (and then backgroundColor)
// parent = `color="red.200"` (and then backgroundColor="blackAlpha.100")

const initializer = parent.getInitializer();
const stack = [parent, initializer] as Node[];
if (!initializer) return box.emptyInitializer(identifier, stack);
const initializer = jsxAttribute.getInitializer();
const stack = [jsxAttribute, initializer] as Node[];
if (!initializer) return box.emptyInitializer(jsxAttribute.getNameNode(), stack);

if (Node.isStringLiteral(initializer)) {
// initializer = `"red.200"` (and then "blackAlpha.100")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { JsxSpreadAttribute, Node } from "ts-morph";
import { createLogger } from "@box-extractor/logger";
import type { JsxSpreadAttribute, Node } from "ts-morph";

import { maybeBoxNode } from "./maybeBoxNode";
import { maybeObjectLikeBox } from "./maybeObjectLikeBox";
import { box } from "./type-factory";
import type { MatchFnPropArgs, MatchPropArgs } from "./types";
import { unwrapExpression } from "./utils";
import { maybeBoxNode } from "./maybeBoxNode";
import type { ListOrAll } from "./types";

const logger = createLogger("box-ex:extractor:jsx-spread");

export const extractJsxSpreadAttributeValues = (spreadAttribute: JsxSpreadAttribute, properties: ListOrAll) => {
export const extractJsxSpreadAttributeValues = (
spreadAttribute: JsxSpreadAttribute,
matchProp: (prop: MatchFnPropArgs | MatchPropArgs) => boolean
) => {
const node = unwrapExpression(spreadAttribute.getExpression());
logger.scoped("extractJsxSpreadAttributeValues", { node: node.getKindName() });

Expand All @@ -25,7 +28,7 @@ export const extractJsxSpreadAttributeValues = (spreadAttribute: JsxSpreadAttrib
}
}

const maybeEntries = maybeObjectLikeBox(node, stack, properties);
const maybeEntries = maybeObjectLikeBox(node, stack, matchProp);
logger({ maybeEntries });
if (maybeEntries) return maybeEntries;

Expand Down
21 changes: 12 additions & 9 deletions packages/box-extractor/src/extractor/maybeObjectLikeBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
BoxNodeObject,
BoxNodeUnresolvable,
} from "./type-factory";
import type { ListOrAll } from "./types";
import type { MatchFnPropArgs } from "./types";
import { isNotNullish, unwrapExpression } from "./utils";

const logger = createLogger("box-ex:extractor:maybe-object");
Expand All @@ -28,7 +28,11 @@ export type MaybeObjectLikeBoxReturn =
| BoxNodeConditional
| undefined;

export const maybeObjectLikeBox = (node: Node, stack: Node[], properties?: ListOrAll): MaybeObjectLikeBoxReturn => {
export const maybeObjectLikeBox = (
node: Node,
stack: Node[],
matchProp?: (prop: MatchFnPropArgs) => boolean
): MaybeObjectLikeBoxReturn => {
const isCached = cacheMap.has(node);
logger({ kind: node.getKindName(), isCached });
if (isCached) {
Expand All @@ -42,7 +46,7 @@ export const maybeObjectLikeBox = (node: Node, stack: Node[], properties?: ListO
};

if (Node.isObjectLiteralExpression(node)) {
return cache(getObjectLiteralExpressionPropPairs(node, stack, properties));
return cache(getObjectLiteralExpressionPropPairs(node, stack, matchProp));
}

// <ColorBox {...(xxx ? yyy : zzz)} />
Expand Down Expand Up @@ -83,11 +87,8 @@ export const maybeObjectLikeBox = (node: Node, stack: Node[], properties?: ListO
const getObjectLiteralExpressionPropPairs = (
expression: ObjectLiteralExpression,
_stack: Node[],
allowed?: ListOrAll
matchProp?: (prop: MatchFnPropArgs) => boolean
) => {
const canTakeAllProp = !allowed || allowed === "all";
const propNameList = allowed ?? [];

const properties = expression.getProperties();
if (properties.length === 0) return box.emptyObject(expression, _stack);

Expand All @@ -109,7 +110,9 @@ const getObjectLiteralExpressionPropPairs = (

const propName = propNameBox.value;
if (!isNotNullish(propName)) return;
if (!canTakeAllProp && !propNameList.includes(propName as string)) return;
if (matchProp && !matchProp?.({ propName: propName as string, propNode: propElement })) {
return;
}

const init = propElement.getInitializer();
if (!init) return;
Expand Down Expand Up @@ -141,7 +144,7 @@ const getObjectLiteralExpressionPropPairs = (
const initializer = unwrapExpression(propElement.getExpression());
stack.push(initializer);

const maybeObject = maybeObjectLikeBox(initializer, stack, allowed);
const maybeObject = maybeObjectLikeBox(initializer, stack, matchProp);
logger("isSpreadAssignment", { extracted: Boolean(maybeObject) });
if (!maybeObject) return;

Expand Down
53 changes: 47 additions & 6 deletions packages/box-extractor/src/extractor/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { CallExpression, JsxOpeningElement, JsxSelfClosingElement, SourceFile } from "ts-morph";
import type {
CallExpression,
JsxAttribute,
JsxOpeningElement,
JsxSelfClosingElement,
PropertyAssignment,
ShorthandPropertyAssignment,
SourceFile,
} from "ts-morph";
import type { BoxNode, BoxNodeList, BoxNodeMap, LiteralValue } from "./type-factory";

export type PrimitiveType = string | number | boolean | null | undefined;
Expand Down Expand Up @@ -28,12 +36,45 @@ export type ExtractResultItem = ExtractedComponentResult | ExtractedFunctionResu
export type ExtractResultByName = Map<string, ExtractResultItem>;

export type ListOrAll = "all" | string[];

export type MatchTagArgs = {
tagName: string;
tagNode: JsxOpeningElement | JsxSelfClosingElement;
isFactory: boolean;
};
export type MatchPropArgs = {
propName: string;
propNode: JsxAttribute | undefined;
};
export type MatchFnArgs = {
fnName: string;
fnNode: CallExpression;
};
export type MatchFnPropArgs = {
propName: string;
propNode: PropertyAssignment | ShorthandPropertyAssignment;
};
export type MatchPropFn = (prop: MatchPropArgs) => boolean;
export type FunctionMatchers = {
matchFn: (element: MatchFnArgs) => boolean;
matchProp: (prop: Pick<MatchFnArgs, "fnName" | "fnNode"> & MatchFnPropArgs) => boolean;
};

export type ComponentMatchers = {
matchTag: (element: MatchTagArgs) => boolean;
matchProp: (prop: Pick<MatchTagArgs, "tagName" | "tagNode"> & MatchPropArgs) => boolean;
};

export type ExtractOptions = {
ast: SourceFile;
components?: Extractable;
functions?: Extractable;
components?: ComponentMatchers;
functions?: FunctionMatchers;
// TODO
// evaluateOptions?: EvaluateOptions;
// flags?: {
// skipEvaluate?: boolean; // TODO allow list of Node.kind ? = [ts.SyntaxKind.CallExpression, ts.SyntaxKind.ConditionalExpression, ts.SyntaxKind.BinaryExpression]
// skipTraverseFiles?: boolean;
// skipConditions?: boolean;
// };
extractMap?: ExtractResultByName;
};

export type ExtractableMap = Record<string, { properties: ListOrAll }>;
export type Extractable = ExtractableMap | string[];
Loading

0 comments on commit cec6e1e

Please sign in to comment.