Skip to content

Commit 227f8ea

Browse files
committed
feat: Add support for resolving destructuring in resolveToValue
1 parent 4e2318e commit 227f8ea

12 files changed

+350
-102
lines changed

src/FileState.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,29 @@ export default class FileState {
6161
this.scope = this.path.scope;
6262
}
6363

64+
/**
65+
* Try to resolve and import with the `name`
66+
*/
6467
import(path: ImportPath, name: string): NodePath | null {
6568
return this.#importer(path, name, this);
6669
}
6770

6871
/**
69-
* Parse a new file
72+
* Parse the content of a new file
73+
* The filename is required so that potential imports inside the content can be correctly resolved
7074
*/
71-
parse(code: string): FileState {
75+
parse(code: string, filename: string): FileState {
7276
const ast = this.#parser(code);
7377

74-
return new FileState(this.opts, {
75-
ast,
76-
code,
77-
importer: this.#importer,
78-
parser: this.#parser,
79-
});
78+
return new FileState(
79+
{ ...this.opts, filename },
80+
{
81+
ast,
82+
code,
83+
importer: this.#importer,
84+
parser: this.#parser,
85+
},
86+
);
8087
}
8188

8289
/**

src/__tests__/__snapshots__/main-test.ts.snap

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,3 +2244,29 @@ Array [
22442244
},
22452245
]
22462246
`;
2247+
2248+
exports[`main fixtures processes component "test-all-imports.tsx" without errors 1`] = `
2249+
Array [
2250+
Object {
2251+
"description": "This is a TS component with imported stuff",
2252+
"displayName": "ImportedExtendedComponent",
2253+
"methods": Array [],
2254+
"props": Object {
2255+
"x": Object {
2256+
"defaultValue": Object {
2257+
"computed": false,
2258+
"value": "\\"string\\"",
2259+
},
2260+
"required": false,
2261+
},
2262+
"y": Object {
2263+
"defaultValue": Object {
2264+
"computed": false,
2265+
"value": "'otherstring'",
2266+
},
2267+
"required": false,
2268+
},
2269+
},
2270+
},
2271+
]
2272+
`;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'otherstring'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const obj = { objDestruct: "string" };
2+
3+
export const { objDestruct } = obj;
4+
5+
export defaultFrom from './other-exports';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import { objDestruct, defaultFrom } from './support/some-exports';
3+
4+
/**
5+
* This is a TS component with imported stuff
6+
*/
7+
export function ImportedExtendedComponent({ x = objDestruct, y = defaultFrom }) {
8+
return <h1>Hello world</h1>;
9+
}

src/__tests__/main-test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,18 @@ describe('main', () => {
197197
// even though it is not the default
198198
describe('fixtures', () => {
199199
const fixturePath = path.join(__dirname, 'fixtures');
200-
const fileNames = fs.readdirSync(fixturePath);
200+
const fileNames = fs.readdirSync(fixturePath, { withFileTypes: true });
201201

202202
for (let i = 0; i < fileNames.length; i++) {
203-
const filePath = path.join(fixturePath, fileNames[i]);
203+
if (fileNames[i].isDirectory()) {
204+
continue;
205+
}
206+
const name = fileNames[i].name;
207+
208+
const filePath = path.join(fixturePath, name);
204209
const fileContent = fs.readFileSync(filePath, 'utf8');
205210

206-
it(`processes component "${fileNames[i]}" without errors`, () => {
211+
it(`processes component "${name}" without errors`, () => {
207212
let result;
208213

209214
expect(() => {

src/importer/makeFsImporter.ts

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import resolve from 'resolve';
33
import { dirname } from 'path';
44
import fs from 'fs';
55
import type { NodePath } from '@babel/traverse';
6-
import type { VariableDeclaration } from '@babel/types';
6+
import type { ExportSpecifier, Identifier, ObjectProperty } from '@babel/types';
77
import type { Importer, ImportPath } from '.';
88
import type FileState from '../FileState';
9+
import { resolveObjectPatternPropertyToValue } from '../utils';
910

1011
function defaultLookupModule(filename: string, basedir: string): string {
1112
return resolve.sync(filename, {
@@ -62,7 +63,7 @@ export default function makeFsImporter(
6263
// Read and parse the code
6364
const src = fs.readFileSync(resolvedSource, 'utf8');
6465

65-
nextState = state.parse(src);
66+
nextState = state.parse(src, resolvedSource);
6667

6768
cache.set(resolvedSource, nextState);
6869
}
@@ -76,81 +77,111 @@ export default function makeFsImporter(
7677
name: string,
7778
seen: Set<string>,
7879
): NodePath | null {
79-
let resultPath: NodePath | null = null;
80+
let resultPath: NodePath | null | undefined;
8081

8182
traverseShallow(state.path, {
8283
ExportNamedDeclaration(path) {
83-
const { declaration, specifiers, source } = path.node;
84-
85-
if (
86-
declaration &&
87-
'id' in declaration &&
88-
declaration.id &&
89-
'name' in declaration.id &&
90-
declaration.id.name === name
91-
) {
92-
resultPath = path.get('declaration') as NodePath;
84+
const declaration = path.get('declaration');
85+
86+
// export const/var ...
87+
if (declaration.hasNode() && declaration.isVariableDeclaration()) {
88+
for (const declPath of declaration.get('declarations')) {
89+
const id = declPath.get('id');
90+
const init = declPath.get('init');
91+
92+
if (id.isIdentifier() && id.node.name === name && init.hasNode()) {
93+
// export const/var a = <init>
94+
95+
resultPath = init;
96+
97+
break;
98+
} else if (id.isObjectPattern()) {
99+
// export const/var { a } = <init>
100+
101+
resultPath = id.get('properties').find(prop => {
102+
if (prop.isObjectProperty()) {
103+
const value = prop.get('value');
104+
105+
return value.isIdentifier() && value.node.name === name;
106+
}
107+
// We don't handle RestElement here yet as complicated
108+
109+
return false;
110+
});
111+
112+
if (resultPath) {
113+
resultPath = resolveObjectPatternPropertyToValue(
114+
resultPath as NodePath<ObjectProperty>,
115+
);
116+
117+
break;
118+
}
119+
}
120+
// ArrayPattern not handled yet
121+
}
93122
} else if (
94-
declaration &&
95-
'declarations' in declaration &&
96-
declaration.declarations
123+
declaration.hasNode() &&
124+
declaration.has('id') &&
125+
(declaration.get('id') as NodePath).isIdentifier() &&
126+
(declaration.get('id') as NodePath<Identifier>).node.name === name
97127
) {
98-
(path.get('declaration') as NodePath<VariableDeclaration>)
99-
.get('declarations')
100-
.forEach(declPath => {
101-
const id = declPath.get('id');
102-
103-
// TODO: ArrayPattern and ObjectPattern
104-
if (
105-
id.isIdentifier() &&
106-
id.node.name === name &&
107-
'init' in declPath.node &&
108-
declPath.node.init
109-
) {
110-
resultPath = declPath.get('init') as NodePath;
111-
}
112-
});
113-
} else if (specifiers) {
114-
path.get('specifiers').forEach(specifierPath => {
115-
if (
116-
'name' in specifierPath.node.exported &&
117-
specifierPath.node.exported.name === name
118-
) {
119-
// TODO TESTME with ExportDefaultSpecifier
120-
if (source) {
121-
const local =
122-
'local' in specifierPath.node
123-
? specifierPath.node.local.name
124-
: 'default';
128+
// export function/class/type/interface/enum ...
129+
130+
resultPath = declaration;
131+
} else if (path.has('specifiers')) {
132+
// export { ... } or export x from ... or export * as x from ...
133+
134+
for (const specifierPath of path.get('specifiers')) {
135+
if (specifierPath.isExportNamespaceSpecifier()) {
136+
continue;
137+
}
138+
const exported = specifierPath.get('exported');
139+
140+
if (exported.isIdentifier() && exported.node.name === name) {
141+
// export ... from ''
142+
if (path.has('source')) {
143+
const local = specifierPath.isExportSpecifier()
144+
? specifierPath.node.local.name
145+
: 'default';
125146

126147
resultPath = resolveImportedValue(path, local, state, seen);
127-
} else if ('local' in specifierPath.node) {
128-
resultPath = specifierPath.get('local') as NodePath;
148+
if (resultPath) {
149+
break;
150+
}
151+
} else {
152+
resultPath = (specifierPath as NodePath<ExportSpecifier>).get(
153+
'local',
154+
);
155+
break;
129156
}
130157
}
131-
});
158+
}
132159
}
133160

134-
return false;
161+
resultPath ? path.stop() : path.skip();
135162
},
136163
ExportDefaultDeclaration(path) {
137164
if (name === 'default') {
138165
resultPath = path.get('declaration');
166+
167+
return path.stop();
139168
}
140169

141-
return false;
170+
path.skip();
142171
},
143172
ExportAllDeclaration(path) {
144173
const resolvedPath = resolveImportedValue(path, name, state, seen);
145174

146175
if (resolvedPath) {
147176
resultPath = resolvedPath;
177+
178+
return path.stop();
148179
}
149180

150-
return false;
181+
path.skip();
151182
},
152183
});
153184

154-
return resultPath;
185+
return resultPath || null;
155186
}
156187
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`resolveObjectPatternPropertyToValue AssignmentExpression resolved basic case 1`] = `
4+
Node {
5+
"extra": Object {
6+
"raw": "\\"string\\"",
7+
"rawValue": "string",
8+
},
9+
"type": "StringLiteral",
10+
"value": "string",
11+
}
12+
`;
13+
14+
exports[`resolveObjectPatternPropertyToValue VariableDeclarator resolved basic case 1`] = `
15+
Node {
16+
"extra": Object {
17+
"raw": "\\"string\\"",
18+
"rawValue": "string",
19+
},
20+
"type": "StringLiteral",
21+
"value": "string",
22+
}
23+
`;

0 commit comments

Comments
 (0)