@khanacademy/graphql-flow 3.4.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/changeset-release.yml +1 -1
- package/.github/workflows/pr-checks.yml +6 -6
- package/CHANGELOG.md +10 -0
- package/dist/cli/run.js +1 -1
- package/dist/parser/parse.js +38 -12
- package/dist/parser/resolve.js +14 -1
- package/dist/parser/resolveImport.js +58 -0
- package/dist/parser/utils.js +2 -15
- package/package.json +71 -65
- package/schema.json +1 -17
- package/src/cli/run.ts +1 -1
- package/src/parser/__test__/parse.test.ts +195 -1
- package/src/parser/__test__/resolveImport.test.ts +98 -0
- package/src/parser/__test__/utils.test.ts +10 -47
- package/src/parser/parse.ts +48 -25
- package/src/parser/resolve.ts +18 -1
- package/src/parser/resolveImport.ts +65 -0
- package/src/parser/utils.ts +1 -15
- package/src/types.ts +0 -4
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
node-version: [20.x]
|
|
18
18
|
steps:
|
|
19
19
|
- uses: actions/checkout@v4
|
|
20
|
-
- uses: Khan/actions@shared-node-cache-
|
|
20
|
+
- uses: Khan/actions@shared-node-cache-v3
|
|
21
21
|
with:
|
|
22
22
|
node-version: ${{ matrix.node-version }}
|
|
23
23
|
|
|
@@ -34,7 +34,7 @@ jobs:
|
|
|
34
34
|
|
|
35
35
|
- name: Run TypeScript
|
|
36
36
|
if: steps.ts-files.outputs.filtered != '[]'
|
|
37
|
-
run:
|
|
37
|
+
run: pnpm typecheck
|
|
38
38
|
|
|
39
39
|
- id: eslint-reset
|
|
40
40
|
uses: Khan/actions@filter-files-v1
|
|
@@ -47,9 +47,9 @@ jobs:
|
|
|
47
47
|
uses: Khan/actions@full-or-limited-v0
|
|
48
48
|
with:
|
|
49
49
|
full-trigger: ${{ steps.eslint-reset.outputs.filtered }}
|
|
50
|
-
full:
|
|
50
|
+
full: pnpm eslint src/**/*.ts
|
|
51
51
|
limited-trigger: ${{ steps.ts-files.outputs.filtered }}
|
|
52
|
-
limited:
|
|
52
|
+
limited: pnpm eslint {}
|
|
53
53
|
|
|
54
54
|
- id: jest-reset
|
|
55
55
|
uses: Khan/actions@filter-files-v1
|
|
@@ -62,6 +62,6 @@ jobs:
|
|
|
62
62
|
uses: Khan/actions@full-or-limited-v0
|
|
63
63
|
with:
|
|
64
64
|
full-trigger: ${{ steps.jest-reset.outputs.filtered }}
|
|
65
|
-
full:
|
|
65
|
+
full: pnpm jest
|
|
66
66
|
limited-trigger: ${{ steps.ts-files.outputs.filtered }}
|
|
67
|
-
limited:
|
|
67
|
+
limited: pnpm jest --findRelatedTests {}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @khanacademy/graphql-flow
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 9d96c11: Overhaul import resolution, relying on tsconfig for import aliases, and rspack-resolver for package imports. This removes the `alias` config option, instead requiring that aliases be defined in a `tsconfig.json`.
|
|
8
|
+
|
|
9
|
+
### Minor Changes
|
|
10
|
+
|
|
11
|
+
- a27caca: Support "export \* from" for fragments
|
|
12
|
+
|
|
3
13
|
## 3.4.2
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/cli/run.js
CHANGED
|
@@ -40,7 +40,7 @@ const inputFiles = (0, _config.getInputFiles)(options, config);
|
|
|
40
40
|
/** Step (2) */
|
|
41
41
|
|
|
42
42
|
const files = (0, _parse.processFiles)(inputFiles, config, f => {
|
|
43
|
-
const resolvedPath = (0, _utils.getPathWithExtension)(f
|
|
43
|
+
const resolvedPath = (0, _utils.getPathWithExtension)(f);
|
|
44
44
|
if (!resolvedPath) {
|
|
45
45
|
throw new Error(`Unable to find ${f}`);
|
|
46
46
|
}
|
package/dist/parser/parse.js
CHANGED
|
@@ -8,6 +8,7 @@ var _wonderStuffCore = require("@khanacademy/wonder-stuff-core");
|
|
|
8
8
|
var _parser = require("@babel/parser");
|
|
9
9
|
var _traverse = _interopRequireDefault(require("@babel/traverse"));
|
|
10
10
|
var _path = _interopRequireDefault(require("path"));
|
|
11
|
+
var _resolveImport = require("./resolveImport");
|
|
11
12
|
var _utils = require("./utils");
|
|
12
13
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
14
|
/**
|
|
@@ -47,12 +48,12 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
47
48
|
* potentially relevant, and of course any values referenced
|
|
48
49
|
* from a graphql template are treated as relevant.
|
|
49
50
|
*/
|
|
50
|
-
const listExternalReferences =
|
|
51
|
+
const listExternalReferences = file => {
|
|
51
52
|
const paths = {};
|
|
52
53
|
const add = (v, followImports) => {
|
|
53
54
|
if (v.type === "import") {
|
|
54
55
|
if (followImports) {
|
|
55
|
-
const absPath = (0, _utils.getPathWithExtension)(v.path
|
|
56
|
+
const absPath = (0, _utils.getPathWithExtension)(v.path);
|
|
56
57
|
if (absPath) {
|
|
57
58
|
paths[absPath] = true;
|
|
58
59
|
}
|
|
@@ -71,14 +72,15 @@ const listExternalReferences = (file, config) => {
|
|
|
71
72
|
file.operations.forEach(op => op.source.expressions.forEach(expr => add(expr,
|
|
72
73
|
// Imports that are used in graphql expressions definitely need to be followed.
|
|
73
74
|
true)));
|
|
75
|
+
file.exportAlls.forEach(expr => add(expr, true));
|
|
74
76
|
return Object.keys(paths);
|
|
75
77
|
};
|
|
76
78
|
const processFile = (filePath, contents, config) => {
|
|
77
|
-
const dir = _path.default.dirname(filePath);
|
|
78
79
|
const result = {
|
|
79
80
|
path: filePath,
|
|
80
81
|
operations: [],
|
|
81
82
|
exports: {},
|
|
83
|
+
exportAlls: [],
|
|
82
84
|
locals: {},
|
|
83
85
|
errors: []
|
|
84
86
|
};
|
|
@@ -94,14 +96,16 @@ const processFile = (filePath, contents, config) => {
|
|
|
94
96
|
ast.program.body.forEach(toplevel => {
|
|
95
97
|
var _toplevel$declaration;
|
|
96
98
|
if (toplevel.type === "ImportDeclaration") {
|
|
97
|
-
const newLocals = getLocals(
|
|
99
|
+
const newLocals = getLocals(toplevel, filePath);
|
|
98
100
|
if (newLocals) {
|
|
99
101
|
Object.keys(newLocals).forEach(k => {
|
|
102
|
+
var _local$resolvedPath$i, _local$resolvedPath;
|
|
100
103
|
const local = newLocals[k];
|
|
101
|
-
|
|
104
|
+
const isGraphqlTagImport = local.rawPath === "graphql-tag" || ((_local$resolvedPath$i = (_local$resolvedPath = local.resolvedPath) === null || _local$resolvedPath === void 0 ? void 0 : _local$resolvedPath.includes(`${_path.default.sep}node_modules${_path.default.sep}graphql-tag`)) !== null && _local$resolvedPath$i !== void 0 ? _local$resolvedPath$i : false);
|
|
105
|
+
if (_path.default.isAbsolute(local.path)) {
|
|
102
106
|
result.locals[k] = local;
|
|
103
107
|
}
|
|
104
|
-
if (
|
|
108
|
+
if (isGraphqlTagImport && local.name === "default") {
|
|
105
109
|
gqlTagNames.push(k);
|
|
106
110
|
}
|
|
107
111
|
});
|
|
@@ -111,7 +115,8 @@ const processFile = (filePath, contents, config) => {
|
|
|
111
115
|
if (toplevel.source) {
|
|
112
116
|
var _toplevel$specifiers;
|
|
113
117
|
const source = toplevel.source;
|
|
114
|
-
const
|
|
118
|
+
const resolvedPath = (0, _resolveImport.resolveImportPath)(filePath, source.value);
|
|
119
|
+
const importPath = resolvedPath !== null && resolvedPath !== void 0 ? resolvedPath : source.value;
|
|
115
120
|
(_toplevel$specifiers = toplevel.specifiers) === null || _toplevel$specifiers === void 0 || _toplevel$specifiers.forEach(spec => {
|
|
116
121
|
if (spec.type === "ExportSpecifier" && spec.exported.type === "Identifier") {
|
|
117
122
|
var _spec$start, _spec$end, _spec$loc$start$line, _spec$loc;
|
|
@@ -140,6 +145,22 @@ const processFile = (filePath, contents, config) => {
|
|
|
140
145
|
});
|
|
141
146
|
}
|
|
142
147
|
}
|
|
148
|
+
if (toplevel.type === "ExportAllDeclaration" && toplevel.source) {
|
|
149
|
+
var _toplevel$start, _toplevel$end, _toplevel$loc$start$l, _toplevel$loc;
|
|
150
|
+
const source = toplevel.source;
|
|
151
|
+
const importPath = source.value.startsWith(".") ? _path.default.resolve(_path.default.join(_path.default.dirname(filePath), source.value)) : source.value;
|
|
152
|
+
result.exportAlls.push({
|
|
153
|
+
type: "import",
|
|
154
|
+
name: "*",
|
|
155
|
+
path: importPath,
|
|
156
|
+
loc: {
|
|
157
|
+
start: (_toplevel$start = toplevel.start) !== null && _toplevel$start !== void 0 ? _toplevel$start : -1,
|
|
158
|
+
end: (_toplevel$end = toplevel.end) !== null && _toplevel$end !== void 0 ? _toplevel$end : -1,
|
|
159
|
+
line: (_toplevel$loc$start$l = (_toplevel$loc = toplevel.loc) === null || _toplevel$loc === void 0 ? void 0 : _toplevel$loc.start.line) !== null && _toplevel$loc$start$l !== void 0 ? _toplevel$loc$start$l : -1,
|
|
160
|
+
path: filePath
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
143
164
|
const processDeclarator = (decl, isExported) => {
|
|
144
165
|
if (decl.id.type !== "Identifier" || !decl.init) {
|
|
145
166
|
return;
|
|
@@ -210,7 +231,8 @@ const processFile = (filePath, contents, config) => {
|
|
|
210
231
|
};
|
|
211
232
|
(0, _traverse.default)(ast, {
|
|
212
233
|
TaggedTemplateExpression(path) {
|
|
213
|
-
|
|
234
|
+
const node = path.node;
|
|
235
|
+
visitTpl(node, name => {
|
|
214
236
|
const binding = path.scope.getBinding(name);
|
|
215
237
|
if (!binding) {
|
|
216
238
|
return null;
|
|
@@ -273,12 +295,12 @@ const processTemplate = (tpl, result, getTemplate) => {
|
|
|
273
295
|
}
|
|
274
296
|
};
|
|
275
297
|
};
|
|
276
|
-
const getLocals = (
|
|
298
|
+
const getLocals = (toplevel, myPath) => {
|
|
277
299
|
if (toplevel.importKind === "type") {
|
|
278
300
|
return null;
|
|
279
301
|
}
|
|
280
|
-
const
|
|
281
|
-
const importPath =
|
|
302
|
+
const resolvedPath = (0, _resolveImport.resolveImportPath)(myPath, toplevel.source.value);
|
|
303
|
+
const importPath = resolvedPath !== null && resolvedPath !== void 0 ? resolvedPath : toplevel.source.value;
|
|
282
304
|
const locals = {};
|
|
283
305
|
toplevel.specifiers.forEach(spec => {
|
|
284
306
|
if (spec.type === "ImportDefaultSpecifier") {
|
|
@@ -286,6 +308,8 @@ const getLocals = (dir, toplevel, myPath, config) => {
|
|
|
286
308
|
type: "import",
|
|
287
309
|
name: "default",
|
|
288
310
|
path: importPath,
|
|
311
|
+
rawPath: toplevel.source.value,
|
|
312
|
+
resolvedPath,
|
|
289
313
|
loc: {
|
|
290
314
|
start: spec.start,
|
|
291
315
|
end: spec.end,
|
|
@@ -297,6 +321,8 @@ const getLocals = (dir, toplevel, myPath, config) => {
|
|
|
297
321
|
type: "import",
|
|
298
322
|
name: spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value,
|
|
299
323
|
path: importPath,
|
|
324
|
+
rawPath: toplevel.source.value,
|
|
325
|
+
resolvedPath,
|
|
300
326
|
loc: {
|
|
301
327
|
start: spec.start,
|
|
302
328
|
end: spec.end,
|
|
@@ -321,7 +347,7 @@ const processFiles = (filePaths, config, getFileSource) => {
|
|
|
321
347
|
}
|
|
322
348
|
const result = processFile(next, source, config);
|
|
323
349
|
files[next] = result;
|
|
324
|
-
listExternalReferences(result
|
|
350
|
+
listExternalReferences(result).forEach(path => {
|
|
325
351
|
if (!files[path] && !toProcess.includes(path)) {
|
|
326
352
|
toProcess.push(path);
|
|
327
353
|
}
|
package/dist/parser/resolve.js
CHANGED
|
@@ -29,7 +29,7 @@ const resolveDocuments = (files, config) => {
|
|
|
29
29
|
};
|
|
30
30
|
exports.resolveDocuments = resolveDocuments;
|
|
31
31
|
const resolveImport = (expr, files, errors, seen, config) => {
|
|
32
|
-
const absPath = (0, _utils.getPathWithExtension)(expr.path
|
|
32
|
+
const absPath = (0, _utils.getPathWithExtension)(expr.path);
|
|
33
33
|
if (!absPath) {
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
@@ -50,6 +50,19 @@ const resolveImport = (expr, files, errors, seen, config) => {
|
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
if (!res.exports[expr.name]) {
|
|
53
|
+
if (expr.name !== "*" && res.exportAlls.length) {
|
|
54
|
+
for (const exportAll of res.exportAlls) {
|
|
55
|
+
const value = resolveImport({
|
|
56
|
+
...exportAll,
|
|
57
|
+
name: expr.name
|
|
58
|
+
}, files, errors, {
|
|
59
|
+
...seen
|
|
60
|
+
}, config);
|
|
61
|
+
if (value) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
53
66
|
errors.push({
|
|
54
67
|
loc: expr.loc,
|
|
55
68
|
message: `${absPath} has no valid gql export ${expr.name}`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.resetImportCache = resetImportCache;
|
|
7
|
+
exports.resolveImportPath = resolveImportPath;
|
|
8
|
+
var _nodePath = _interopRequireDefault(require("node:path"));
|
|
9
|
+
var _rspackResolver = require("rspack-resolver");
|
|
10
|
+
var _tsconfigPaths = require("tsconfig-paths");
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
// Copied from https://github.com/Khan/frontend/blob/main/libs/node/resolve-import/src/resolve-import.ts
|
|
13
|
+
// In future it would be cool to publish @khan/node-resolve-import as a public library so we
|
|
14
|
+
// could consume it here.
|
|
15
|
+
|
|
16
|
+
const CONDITION_NAMES = ["import"];
|
|
17
|
+
const matchPathCache = new Map();
|
|
18
|
+
let esmResolver = getEsmResolver();
|
|
19
|
+
function getEsmResolver() {
|
|
20
|
+
return new _rspackResolver.ResolverFactory({
|
|
21
|
+
conditionNames: CONDITION_NAMES,
|
|
22
|
+
mainFields: ["module", "main"],
|
|
23
|
+
extensions: [".mjs", ".js", ".jsx", ".ts", ".tsx"]
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function resetImportCache() {
|
|
27
|
+
matchPathCache.clear();
|
|
28
|
+
esmResolver = getEsmResolver();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolves an import path using tsconfig and the rspack resolver.
|
|
33
|
+
*
|
|
34
|
+
* @param sourceFile - The file that is importing the path.
|
|
35
|
+
* @param importPath - The path to resolve.
|
|
36
|
+
* @returns The fully resolved path.
|
|
37
|
+
*/
|
|
38
|
+
function resolveImportPath(sourceFile, importPath) {
|
|
39
|
+
const dir = _nodePath.default.dirname(sourceFile);
|
|
40
|
+
let matchPath = matchPathCache.get(dir);
|
|
41
|
+
if (!matchPath) {
|
|
42
|
+
const foundConfig = (0, _tsconfigPaths.loadConfig)(dir);
|
|
43
|
+
if (foundConfig.resultType !== "success") {
|
|
44
|
+
throw new Error("Failed to load tsconfig");
|
|
45
|
+
}
|
|
46
|
+
matchPath = (0, _tsconfigPaths.createMatchPath)(foundConfig.absoluteBaseUrl, foundConfig.paths, CONDITION_NAMES);
|
|
47
|
+
matchPathCache.set(dir, matchPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// See if we can resolve the import path using the tsconfig.
|
|
51
|
+
const resolvedPath = matchPath(importPath);
|
|
52
|
+
|
|
53
|
+
// Get the directory of the source file to resolve against.
|
|
54
|
+
const sourceFileDir = _nodePath.default.dirname(sourceFile);
|
|
55
|
+
|
|
56
|
+
// Get the fully resolved path.
|
|
57
|
+
return esmResolver.sync(sourceFileDir, resolvedPath !== null && resolvedPath !== void 0 ? resolvedPath : importPath).path;
|
|
58
|
+
}
|
package/dist/parser/utils.js
CHANGED
|
@@ -3,23 +3,10 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.getPathWithExtension =
|
|
6
|
+
exports.getPathWithExtension = void 0;
|
|
7
7
|
var _fs = _interopRequireDefault(require("fs"));
|
|
8
8
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
-
const
|
|
10
|
-
if (config.alias) {
|
|
11
|
-
for (const {
|
|
12
|
-
find,
|
|
13
|
-
replacement
|
|
14
|
-
} of config.alias) {
|
|
15
|
-
path = path.replace(find, replacement);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return path;
|
|
19
|
-
};
|
|
20
|
-
exports.fixPathResolution = fixPathResolution;
|
|
21
|
-
const getPathWithExtension = (pathWithoutExtension, config) => {
|
|
22
|
-
pathWithoutExtension = fixPathResolution(pathWithoutExtension, config);
|
|
9
|
+
const getPathWithExtension = pathWithoutExtension => {
|
|
23
10
|
if (/\.(less|css|png|gif|jpg|jpeg|js|jsx|ts|tsx|mjs)$/.test(pathWithoutExtension)) {
|
|
24
11
|
return pathWithoutExtension;
|
|
25
12
|
}
|
package/package.json
CHANGED
|
@@ -1,66 +1,72 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
2
|
+
"name": "@khanacademy/graphql-flow",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/Khan/graphql-flow.git"
|
|
7
|
+
},
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/Khan/graphql-flow/issues"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"provenance": true
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"graphql-flow": "dist/cli/run.js"
|
|
17
|
+
},
|
|
18
|
+
"jest": {
|
|
19
|
+
"testPathIgnorePatterns": [
|
|
20
|
+
"dist"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@babel/cli": "^7.17.6",
|
|
26
|
+
"@babel/eslint-parser": "^7.17.0",
|
|
27
|
+
"@babel/polyfill": "^7.0.0",
|
|
28
|
+
"@babel/preset-env": "^7.24.5",
|
|
29
|
+
"@babel/preset-typescript": "^7.24.1",
|
|
30
|
+
"@changesets/cli": "^2.21.1",
|
|
31
|
+
"@jest/globals": "^30.2.0",
|
|
32
|
+
"@khanacademy/eslint-config": "^4.0.0",
|
|
33
|
+
"@types/babel__generator": "^7.27.0",
|
|
34
|
+
"@types/babel__traverse": "^7.28.0",
|
|
35
|
+
"@types/jest": "^29.5.3",
|
|
36
|
+
"@types/minimist": "^1.2.5",
|
|
37
|
+
"@types/prop-types": "^15.7.12",
|
|
38
|
+
"@types/react": "^18.3.3",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
|
40
|
+
"@typescript-eslint/parser": "^7.17.0",
|
|
41
|
+
"babel-jest": "23.4.2",
|
|
42
|
+
"eslint": "^8.57.0",
|
|
43
|
+
"eslint-config-prettier": "7.0.0",
|
|
44
|
+
"eslint-plugin-jest": "^28.6.0",
|
|
45
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
46
|
+
"graphql-tag": "2.10.1",
|
|
47
|
+
"jest": "^27.5.1",
|
|
48
|
+
"prettier": "^2.5.1",
|
|
49
|
+
"prettier-eslint": "^13.0.0",
|
|
50
|
+
"typescript": "^5.1.6"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@babel/core": "^7.24.5",
|
|
54
|
+
"@babel/generator": "^7.24.5",
|
|
55
|
+
"@babel/parser": "^7.24.5",
|
|
56
|
+
"@babel/traverse": "^7.24.5",
|
|
57
|
+
"@babel/types": "^7.24.5",
|
|
58
|
+
"@khanacademy/wonder-stuff-core": "^1.5.1",
|
|
59
|
+
"apollo-utilities": "^1.3.4",
|
|
60
|
+
"graphql": "^16.9.0",
|
|
61
|
+
"jsonschema": "^1.4.1",
|
|
62
|
+
"minimist": "^1.2.8",
|
|
63
|
+
"rspack-resolver": "^1.3.0",
|
|
64
|
+
"tsconfig-paths": "^4.2.0"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"test": "jest",
|
|
68
|
+
"typecheck": "tsc --noEmit",
|
|
69
|
+
"publish:ci": "pnpm build && changeset publish",
|
|
70
|
+
"build": "babel src --extensions '.ts, .tsx' --out-dir dist --ignore 'src/**/*.spec.ts','src/**/*.test.ts' && chmod 755 dist/cli/run.js"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/schema.json
CHANGED
|
@@ -94,23 +94,7 @@
|
|
|
94
94
|
"generate": {"oneOf": [
|
|
95
95
|
{"$ref": "#/definitions/GenerateConfig"},
|
|
96
96
|
{"type": "array", "items": {"$ref": "#/definitions/GenerateConfig"}}
|
|
97
|
-
]}
|
|
98
|
-
"alias": {
|
|
99
|
-
"type": "array",
|
|
100
|
-
"items": {
|
|
101
|
-
"type": "object",
|
|
102
|
-
"additionalProperties": false,
|
|
103
|
-
"properties": {
|
|
104
|
-
"find": {
|
|
105
|
-
"type": ["string", "object"]
|
|
106
|
-
},
|
|
107
|
-
"replacement": {
|
|
108
|
-
"type": "string"
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
"required": [ "find", "replacement" ]
|
|
112
|
-
}
|
|
113
|
-
}
|
|
97
|
+
]}
|
|
114
98
|
},
|
|
115
99
|
"required": [ "crawl", "generate" ]
|
|
116
100
|
}
|
package/src/cli/run.ts
CHANGED
|
@@ -56,7 +56,7 @@ const inputFiles = getInputFiles(options, config);
|
|
|
56
56
|
/** Step (2) */
|
|
57
57
|
|
|
58
58
|
const files = processFiles(inputFiles, config, (f) => {
|
|
59
|
-
const resolvedPath = getPathWithExtension(f
|
|
59
|
+
const resolvedPath = getPathWithExtension(f);
|
|
60
60
|
if (!resolvedPath) {
|
|
61
61
|
throw new Error(`Unable to find ${f}`);
|
|
62
62
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import {describe, it, expect, afterEach} from "@jest/globals";
|
|
2
5
|
|
|
3
6
|
import {Config} from "../../types";
|
|
4
7
|
import {processFiles} from "../parse";
|
|
@@ -6,6 +9,32 @@ import {resolveDocuments} from "../resolve";
|
|
|
6
9
|
|
|
7
10
|
import {print} from "graphql/language/printer";
|
|
8
11
|
|
|
12
|
+
jest.mock("../resolveImport", () => ({
|
|
13
|
+
resetImportCache: jest.fn(),
|
|
14
|
+
resolveImportPath: jest
|
|
15
|
+
.fn()
|
|
16
|
+
.mockImplementation((sourceFile: string, importPath: string) => {
|
|
17
|
+
const path = require("path");
|
|
18
|
+
if (importPath === "graphql-tag") {
|
|
19
|
+
return "/repo/node_modules/graphql-tag/index.js";
|
|
20
|
+
}
|
|
21
|
+
if (importPath === "monorepo-package/fragment") {
|
|
22
|
+
return "/repo/node_modules/monorepo-package/fragment.js";
|
|
23
|
+
}
|
|
24
|
+
if (importPath.startsWith(".")) {
|
|
25
|
+
return path.resolve(path.dirname(sourceFile), importPath);
|
|
26
|
+
}
|
|
27
|
+
if (path.isAbsolute(importPath)) {
|
|
28
|
+
return importPath;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
9
38
|
const fixtureFiles: {
|
|
10
39
|
[key: string]:
|
|
11
40
|
| string
|
|
@@ -14,6 +43,27 @@ const fixtureFiles: {
|
|
|
14
43
|
resolvedPath: string;
|
|
15
44
|
};
|
|
16
45
|
} = {
|
|
46
|
+
"/repo/node_modules/monorepo-package/fragment.js": `
|
|
47
|
+
import gql from 'graphql-tag';
|
|
48
|
+
|
|
49
|
+
export const sharedFragment = gql\`
|
|
50
|
+
fragment SharedFields on Something {
|
|
51
|
+
id
|
|
52
|
+
}
|
|
53
|
+
\`;
|
|
54
|
+
`,
|
|
55
|
+
"/repo/packages/app/App.js": `
|
|
56
|
+
import gql from 'graphql-tag';
|
|
57
|
+
import {sharedFragment} from 'monorepo-package/fragment';
|
|
58
|
+
export const appQuery = gql\`
|
|
59
|
+
query AppQuery {
|
|
60
|
+
viewer {
|
|
61
|
+
...SharedFields
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
\${sharedFragment}
|
|
65
|
+
\`;
|
|
66
|
+
`,
|
|
17
67
|
"/firstFile.js": `
|
|
18
68
|
// Note that you can import graphql-tag as
|
|
19
69
|
// something other than gql.
|
|
@@ -57,6 +107,31 @@ const fixtureFiles: {
|
|
|
57
107
|
\`;
|
|
58
108
|
export {secondFragment};`,
|
|
59
109
|
|
|
110
|
+
"/starExportSource.js": `
|
|
111
|
+
import gql from 'graphql-tag';
|
|
112
|
+
export const starFragment = gql\`
|
|
113
|
+
fragment StarFragment on Star {
|
|
114
|
+
id
|
|
115
|
+
}
|
|
116
|
+
\`;
|
|
117
|
+
`,
|
|
118
|
+
"/starExportReexport.js": `
|
|
119
|
+
export * from './starExportSource.js';
|
|
120
|
+
`,
|
|
121
|
+
"/starExportConsumer.js": `
|
|
122
|
+
import gql from 'graphql-tag';
|
|
123
|
+
import {starFragment} from './starExportReexport.js';
|
|
124
|
+
|
|
125
|
+
export const starQuery = gql\`
|
|
126
|
+
query StarQuery {
|
|
127
|
+
stars {
|
|
128
|
+
...StarFragment
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
\${starFragment}
|
|
132
|
+
\`;
|
|
133
|
+
`,
|
|
134
|
+
|
|
60
135
|
"/thirdFile.js": `
|
|
61
136
|
import {fromFirstFile, alsoFirst, secondFragment} from './secondFile.js';
|
|
62
137
|
import gql from 'graphql-tag';
|
|
@@ -338,4 +413,123 @@ describe("processing fragments in various ways", () => {
|
|
|
338
413
|
);
|
|
339
414
|
expect(printed).toMatchInlineSnapshot(`Object {}`);
|
|
340
415
|
});
|
|
416
|
+
|
|
417
|
+
it("should resolve fragments re-exported via export all", () => {
|
|
418
|
+
// Arrange
|
|
419
|
+
const config: Config = {
|
|
420
|
+
crawl: {
|
|
421
|
+
root: "/here/we/crawl",
|
|
422
|
+
},
|
|
423
|
+
generate: {
|
|
424
|
+
match: [/\.fixture\.js$/],
|
|
425
|
+
exclude: [
|
|
426
|
+
"_test\\.js$",
|
|
427
|
+
"\\bcourse-editor-package\\b",
|
|
428
|
+
"\\.fixture\\.js$",
|
|
429
|
+
"\\b__flowtests__\\b",
|
|
430
|
+
"\\bcourse-editor\\b",
|
|
431
|
+
],
|
|
432
|
+
readOnlyArray: false,
|
|
433
|
+
regenerateCommand: "make gqlflow",
|
|
434
|
+
scalars: {
|
|
435
|
+
JSONString: "string",
|
|
436
|
+
KALocale: "string",
|
|
437
|
+
NaiveDateTime: "string",
|
|
438
|
+
},
|
|
439
|
+
splitTypes: true,
|
|
440
|
+
generatedDirectory: "__graphql-types__",
|
|
441
|
+
exportAllObjectTypes: true,
|
|
442
|
+
schemaFilePath: "./composed_schema.graphql",
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
// Act
|
|
446
|
+
const files = processFiles(
|
|
447
|
+
["/starExportConsumer.js"],
|
|
448
|
+
config,
|
|
449
|
+
getFileSource,
|
|
450
|
+
);
|
|
451
|
+
const {resolved} = resolveDocuments(files, config);
|
|
452
|
+
const printed: Record<string, any> = {};
|
|
453
|
+
Object.keys(resolved).map(
|
|
454
|
+
(k: any) => (printed[k] = print(resolved[k].document).trim()),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Assert
|
|
458
|
+
expect(printed).toMatchInlineSnapshot(`
|
|
459
|
+
Object {
|
|
460
|
+
"/starExportConsumer.js:5": "query StarQuery {
|
|
461
|
+
stars {
|
|
462
|
+
...StarFragment
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fragment StarFragment on Star {
|
|
467
|
+
id
|
|
468
|
+
}",
|
|
469
|
+
"/starExportSource.js:3": "fragment StarFragment on Star {
|
|
470
|
+
id
|
|
471
|
+
}",
|
|
472
|
+
}
|
|
473
|
+
`);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("should resolve fragments imported from monorepo packages", () => {
|
|
477
|
+
// Arrange
|
|
478
|
+
const config: Config = {
|
|
479
|
+
crawl: {
|
|
480
|
+
root: "/here/we/crawl",
|
|
481
|
+
},
|
|
482
|
+
generate: {
|
|
483
|
+
match: [/\.fixture\.js$/],
|
|
484
|
+
exclude: [
|
|
485
|
+
"_test\\.js$",
|
|
486
|
+
"\\bcourse-editor-package\\b",
|
|
487
|
+
"\\.fixture\\.js$",
|
|
488
|
+
"\\b__flowtests__\\b",
|
|
489
|
+
"\\bcourse-editor\\b",
|
|
490
|
+
],
|
|
491
|
+
readOnlyArray: false,
|
|
492
|
+
regenerateCommand: "make gqlflow",
|
|
493
|
+
scalars: {
|
|
494
|
+
JSONString: "string",
|
|
495
|
+
KALocale: "string",
|
|
496
|
+
NaiveDateTime: "string",
|
|
497
|
+
},
|
|
498
|
+
splitTypes: true,
|
|
499
|
+
generatedDirectory: "__graphql-types__",
|
|
500
|
+
exportAllObjectTypes: true,
|
|
501
|
+
schemaFilePath: "./composed_schema.graphql",
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Act
|
|
506
|
+
const files = processFiles(
|
|
507
|
+
["/repo/packages/app/App.js"],
|
|
508
|
+
config,
|
|
509
|
+
getFileSource,
|
|
510
|
+
);
|
|
511
|
+
const {resolved} = resolveDocuments(files, config);
|
|
512
|
+
const printed: Record<string, any> = {};
|
|
513
|
+
Object.keys(resolved).map(
|
|
514
|
+
(k: any) => (printed[k] = print(resolved[k].document).trim()),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// Assert
|
|
518
|
+
expect(printed).toMatchInlineSnapshot(`
|
|
519
|
+
Object {
|
|
520
|
+
"/repo/node_modules/monorepo-package/fragment.js:4": "fragment SharedFields on Something {
|
|
521
|
+
id
|
|
522
|
+
}",
|
|
523
|
+
"/repo/packages/app/App.js:4": "query AppQuery {
|
|
524
|
+
viewer {
|
|
525
|
+
...SharedFields
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
fragment SharedFields on Something {
|
|
530
|
+
id
|
|
531
|
+
}",
|
|
532
|
+
}
|
|
533
|
+
`);
|
|
534
|
+
});
|
|
341
535
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, it} from "@jest/globals";
|
|
5
|
+
import {resolveImportPath, resetImportCache} from "../resolveImport";
|
|
6
|
+
import {ResolverFactory} from "rspack-resolver";
|
|
7
|
+
|
|
8
|
+
const mockSync = jest.fn();
|
|
9
|
+
const mockCreateMatchPath = jest.fn();
|
|
10
|
+
const mockLoadConfig = jest.fn();
|
|
11
|
+
|
|
12
|
+
jest.mock("rspack-resolver", () => ({
|
|
13
|
+
ResolverFactory: jest.fn().mockImplementation(() => ({
|
|
14
|
+
sync: (...args: Array<unknown>) => mockSync(...args),
|
|
15
|
+
})),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.mock("tsconfig-paths", () => ({
|
|
19
|
+
createMatchPath: jest
|
|
20
|
+
.fn()
|
|
21
|
+
.mockImplementation((...args: Array<unknown>) =>
|
|
22
|
+
mockCreateMatchPath(...args),
|
|
23
|
+
),
|
|
24
|
+
loadConfig: jest
|
|
25
|
+
.fn()
|
|
26
|
+
.mockImplementation((...args: Array<unknown>) =>
|
|
27
|
+
mockLoadConfig(...args),
|
|
28
|
+
),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
describe("resolveImportPath", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
mockSync.mockReset();
|
|
34
|
+
mockCreateMatchPath.mockReset();
|
|
35
|
+
mockLoadConfig.mockReset();
|
|
36
|
+
(ResolverFactory as unknown as jest.Mock).mockClear();
|
|
37
|
+
resetImportCache();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns the resolved path from the resolver", () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const matchPath = jest.fn().mockReturnValue("/repo/src/alias/thing.ts");
|
|
47
|
+
mockLoadConfig.mockReturnValue({
|
|
48
|
+
resultType: "success",
|
|
49
|
+
absoluteBaseUrl: "/repo",
|
|
50
|
+
paths: {"@/*": ["src/*"]},
|
|
51
|
+
});
|
|
52
|
+
mockCreateMatchPath.mockReturnValue(matchPath);
|
|
53
|
+
mockSync.mockReturnValue({path: "/repo/src/alias/thing.ts"});
|
|
54
|
+
|
|
55
|
+
// Act
|
|
56
|
+
const result = resolveImportPath("/repo/src/file.ts", "@/alias/thing");
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
expect(result).toBe("/repo/src/alias/thing.ts");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("loads tsconfig from the source file directory", () => {
|
|
63
|
+
// Arrange
|
|
64
|
+
const matchPath = jest.fn().mockReturnValue("/repo/src/alias/thing.ts");
|
|
65
|
+
mockLoadConfig.mockReturnValue({
|
|
66
|
+
resultType: "success",
|
|
67
|
+
absoluteBaseUrl: "/repo",
|
|
68
|
+
paths: {"@/*": ["src/*"]},
|
|
69
|
+
});
|
|
70
|
+
mockCreateMatchPath.mockReturnValue(matchPath);
|
|
71
|
+
mockSync.mockReturnValue({path: "/repo/src/alias/thing.ts"});
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
resolveImportPath("/repo/src/file.ts", "@/alias/thing");
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(mockLoadConfig).toHaveBeenCalledWith("/repo/src");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("caches tsconfig matchPath per source directory", () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
const matchPath = jest.fn().mockReturnValue(undefined);
|
|
83
|
+
mockLoadConfig.mockReturnValue({
|
|
84
|
+
resultType: "success",
|
|
85
|
+
absoluteBaseUrl: "/repo",
|
|
86
|
+
paths: {},
|
|
87
|
+
});
|
|
88
|
+
mockCreateMatchPath.mockReturnValue(matchPath);
|
|
89
|
+
mockSync.mockReturnValue({path: "/repo/node_modules/pkg/index.js"});
|
|
90
|
+
|
|
91
|
+
// Act
|
|
92
|
+
resolveImportPath("/repo/src/a.ts", "pkg");
|
|
93
|
+
resolveImportPath("/repo/src/b.ts", "pkg");
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(mockLoadConfig).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -1,41 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
1
4
|
import fs from "fs";
|
|
2
|
-
import {describe, it, expect
|
|
3
|
-
import type {Config} from "../../types";
|
|
4
|
-
|
|
5
|
+
import {describe, it, expect} from "@jest/globals";
|
|
5
6
|
import {getPathWithExtension} from "../utils";
|
|
6
7
|
|
|
7
|
-
const generate = {
|
|
8
|
-
match: [/\.fixture\.js$/],
|
|
9
|
-
exclude: [
|
|
10
|
-
"_test\\.js$",
|
|
11
|
-
"\\bcourse-editor-package\\b",
|
|
12
|
-
"\\.fixture\\.js$",
|
|
13
|
-
"\\b__flowtests__\\b",
|
|
14
|
-
"\\bcourse-editor\\b",
|
|
15
|
-
],
|
|
16
|
-
readOnlyArray: false,
|
|
17
|
-
regenerateCommand: "make gqlflow",
|
|
18
|
-
scalars: {
|
|
19
|
-
JSONString: "string",
|
|
20
|
-
KALocale: "string",
|
|
21
|
-
NaiveDateTime: "string",
|
|
22
|
-
},
|
|
23
|
-
splitTypes: true,
|
|
24
|
-
generatedDirectory: "__graphql-types__",
|
|
25
|
-
exportAllObjectTypes: true,
|
|
26
|
-
schemaFilePath: "./composed_schema.graphql",
|
|
27
|
-
} as const;
|
|
28
|
-
|
|
29
|
-
const config: Config = {
|
|
30
|
-
crawl: {
|
|
31
|
-
root: "/here/we/crawl",
|
|
32
|
-
},
|
|
33
|
-
generate: [
|
|
34
|
-
{...generate, match: [/^static/], exportAllObjectTypes: false},
|
|
35
|
-
generate,
|
|
36
|
-
],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
8
|
describe("getPathWithExtension", () => {
|
|
40
9
|
it("should handle a basic missing extension", () => {
|
|
41
10
|
// Arrange
|
|
@@ -44,7 +13,7 @@ describe("getPathWithExtension", () => {
|
|
|
44
13
|
);
|
|
45
14
|
|
|
46
15
|
// Act
|
|
47
|
-
const result = getPathWithExtension("/path/to/file"
|
|
16
|
+
const result = getPathWithExtension("/path/to/file");
|
|
48
17
|
|
|
49
18
|
// Assert
|
|
50
19
|
expect(result).toBe("/path/to/file.js");
|
|
@@ -55,26 +24,20 @@ describe("getPathWithExtension", () => {
|
|
|
55
24
|
jest.spyOn(fs, "existsSync").mockImplementation((path) => false);
|
|
56
25
|
|
|
57
26
|
// Act
|
|
58
|
-
const result = getPathWithExtension("/path/to/file"
|
|
27
|
+
const result = getPathWithExtension("/path/to/file");
|
|
59
28
|
|
|
60
29
|
// Assert
|
|
61
30
|
expect(result).toBe(null);
|
|
62
31
|
});
|
|
63
32
|
|
|
64
|
-
it("
|
|
33
|
+
it("returns the original path when an extension is already present", () => {
|
|
65
34
|
// Arrange
|
|
66
|
-
|
|
67
|
-
typeof path === "string" ? path.endsWith(".js") : false,
|
|
68
|
-
);
|
|
69
|
-
const tmpConfig: Config = {
|
|
70
|
-
...config,
|
|
71
|
-
alias: [{find: "~", replacement: "../../some/prefix"}],
|
|
72
|
-
};
|
|
35
|
+
const input = "/dir/file.tsx";
|
|
73
36
|
|
|
74
37
|
// Act
|
|
75
|
-
const result = getPathWithExtension(
|
|
38
|
+
const result = getPathWithExtension(input);
|
|
76
39
|
|
|
77
40
|
// Assert
|
|
78
|
-
expect(result).toBe(
|
|
41
|
+
expect(result).toBe(input);
|
|
79
42
|
});
|
|
80
43
|
});
|
package/src/parser/parse.ts
CHANGED
|
@@ -8,10 +8,12 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
import {parse, ParserPlugin} from "@babel/parser";
|
|
10
10
|
import traverse from "@babel/traverse";
|
|
11
|
+
import type {NodePath} from "@babel/traverse";
|
|
11
12
|
|
|
12
13
|
import path from "path";
|
|
13
14
|
|
|
14
|
-
import {
|
|
15
|
+
import {resolveImportPath} from "./resolveImport";
|
|
16
|
+
import {getPathWithExtension} from "./utils";
|
|
15
17
|
import {Config} from "../types";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -63,6 +65,8 @@ export type Import = {
|
|
|
63
65
|
type: "import";
|
|
64
66
|
name: string;
|
|
65
67
|
path: string;
|
|
68
|
+
rawPath?: string;
|
|
69
|
+
resolvedPath?: string | null;
|
|
66
70
|
loc: Loc;
|
|
67
71
|
};
|
|
68
72
|
|
|
@@ -79,6 +83,7 @@ export type FileResult = {
|
|
|
79
83
|
exports: {
|
|
80
84
|
[key: string]: Document | Import;
|
|
81
85
|
};
|
|
86
|
+
exportAlls: Array<Import>;
|
|
82
87
|
locals: {
|
|
83
88
|
[key: string]: Document | Import;
|
|
84
89
|
};
|
|
@@ -100,15 +105,12 @@ export type Files = {
|
|
|
100
105
|
* potentially relevant, and of course any values referenced
|
|
101
106
|
* from a graphql template are treated as relevant.
|
|
102
107
|
*/
|
|
103
|
-
const listExternalReferences = (
|
|
104
|
-
file: FileResult,
|
|
105
|
-
config: Config,
|
|
106
|
-
): Array<string> => {
|
|
108
|
+
const listExternalReferences = (file: FileResult): Array<string> => {
|
|
107
109
|
const paths: Record<string, any> = {};
|
|
108
110
|
const add = (v: Document | Import, followImports: boolean) => {
|
|
109
111
|
if (v.type === "import") {
|
|
110
112
|
if (followImports) {
|
|
111
|
-
const absPath = getPathWithExtension(v.path
|
|
113
|
+
const absPath = getPathWithExtension(v.path);
|
|
112
114
|
if (absPath) {
|
|
113
115
|
paths[absPath] = true;
|
|
114
116
|
}
|
|
@@ -141,6 +143,7 @@ const listExternalReferences = (
|
|
|
141
143
|
),
|
|
142
144
|
),
|
|
143
145
|
);
|
|
146
|
+
file.exportAlls.forEach((expr) => add(expr, true));
|
|
144
147
|
return Object.keys(paths);
|
|
145
148
|
};
|
|
146
149
|
|
|
@@ -154,11 +157,11 @@ export const processFile = (
|
|
|
154
157
|
},
|
|
155
158
|
config: Config,
|
|
156
159
|
): FileResult => {
|
|
157
|
-
const dir = path.dirname(filePath);
|
|
158
160
|
const result: FileResult = {
|
|
159
161
|
path: filePath,
|
|
160
162
|
operations: [],
|
|
161
163
|
exports: {},
|
|
164
|
+
exportAlls: [],
|
|
162
165
|
locals: {},
|
|
163
166
|
errors: [],
|
|
164
167
|
};
|
|
@@ -178,17 +181,20 @@ export const processFile = (
|
|
|
178
181
|
|
|
179
182
|
ast.program.body.forEach((toplevel) => {
|
|
180
183
|
if (toplevel.type === "ImportDeclaration") {
|
|
181
|
-
const newLocals = getLocals(
|
|
184
|
+
const newLocals = getLocals(toplevel, filePath);
|
|
182
185
|
if (newLocals) {
|
|
183
186
|
Object.keys(newLocals).forEach((k) => {
|
|
184
187
|
const local = newLocals[k];
|
|
185
|
-
|
|
188
|
+
const isGraphqlTagImport =
|
|
189
|
+
local.rawPath === "graphql-tag" ||
|
|
190
|
+
(local.resolvedPath?.includes(
|
|
191
|
+
`${path.sep}node_modules${path.sep}graphql-tag`,
|
|
192
|
+
) ??
|
|
193
|
+
false);
|
|
194
|
+
if (path.isAbsolute(local.path)) {
|
|
186
195
|
result.locals[k] = local;
|
|
187
196
|
}
|
|
188
|
-
if (
|
|
189
|
-
local.path === "graphql-tag" &&
|
|
190
|
-
local.name === "default"
|
|
191
|
-
) {
|
|
197
|
+
if (isGraphqlTagImport && local.name === "default") {
|
|
192
198
|
gqlTagNames.push(k);
|
|
193
199
|
}
|
|
194
200
|
});
|
|
@@ -197,9 +203,8 @@ export const processFile = (
|
|
|
197
203
|
if (toplevel.type === "ExportNamedDeclaration") {
|
|
198
204
|
if (toplevel.source) {
|
|
199
205
|
const source = toplevel.source;
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
: source.value;
|
|
206
|
+
const resolvedPath = resolveImportPath(filePath, source.value);
|
|
207
|
+
const importPath = resolvedPath ?? source.value;
|
|
203
208
|
toplevel.specifiers?.forEach((spec) => {
|
|
204
209
|
if (
|
|
205
210
|
spec.type === "ExportSpecifier" &&
|
|
@@ -229,6 +234,23 @@ export const processFile = (
|
|
|
229
234
|
});
|
|
230
235
|
}
|
|
231
236
|
}
|
|
237
|
+
if (toplevel.type === "ExportAllDeclaration" && toplevel.source) {
|
|
238
|
+
const source = toplevel.source;
|
|
239
|
+
const importPath = source.value.startsWith(".")
|
|
240
|
+
? path.resolve(path.join(path.dirname(filePath), source.value))
|
|
241
|
+
: source.value;
|
|
242
|
+
result.exportAlls.push({
|
|
243
|
+
type: "import",
|
|
244
|
+
name: "*",
|
|
245
|
+
path: importPath,
|
|
246
|
+
loc: {
|
|
247
|
+
start: toplevel.start ?? -1,
|
|
248
|
+
end: toplevel.end ?? -1,
|
|
249
|
+
line: toplevel.loc?.start.line ?? -1,
|
|
250
|
+
path: filePath,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
232
254
|
|
|
233
255
|
const processDeclarator = (
|
|
234
256
|
decl: VariableDeclarator,
|
|
@@ -308,8 +330,9 @@ export const processFile = (
|
|
|
308
330
|
};
|
|
309
331
|
|
|
310
332
|
traverse(ast as any, {
|
|
311
|
-
TaggedTemplateExpression(path) {
|
|
312
|
-
|
|
333
|
+
TaggedTemplateExpression(path: NodePath) {
|
|
334
|
+
const node = path.node as TaggedTemplateExpression;
|
|
335
|
+
visitTpl(node, (name) => {
|
|
313
336
|
const binding = path.scope.getBinding(name);
|
|
314
337
|
if (!binding) {
|
|
315
338
|
return null;
|
|
@@ -384,10 +407,8 @@ const processTemplate = (
|
|
|
384
407
|
};
|
|
385
408
|
|
|
386
409
|
const getLocals = (
|
|
387
|
-
dir: string,
|
|
388
410
|
toplevel: ImportDeclaration,
|
|
389
411
|
myPath: string,
|
|
390
|
-
config: Config,
|
|
391
412
|
):
|
|
392
413
|
| {
|
|
393
414
|
[key: string]: Import;
|
|
@@ -397,10 +418,8 @@ const getLocals = (
|
|
|
397
418
|
if (toplevel.importKind === "type") {
|
|
398
419
|
return null;
|
|
399
420
|
}
|
|
400
|
-
const
|
|
401
|
-
const importPath =
|
|
402
|
-
? path.resolve(path.join(dir, fixedPath))
|
|
403
|
-
: fixedPath;
|
|
421
|
+
const resolvedPath = resolveImportPath(myPath, toplevel.source.value);
|
|
422
|
+
const importPath = resolvedPath ?? toplevel.source.value;
|
|
404
423
|
const locals: Record<string, any> = {};
|
|
405
424
|
toplevel.specifiers.forEach((spec) => {
|
|
406
425
|
if (spec.type === "ImportDefaultSpecifier") {
|
|
@@ -408,6 +427,8 @@ const getLocals = (
|
|
|
408
427
|
type: "import",
|
|
409
428
|
name: "default",
|
|
410
429
|
path: importPath,
|
|
430
|
+
rawPath: toplevel.source.value,
|
|
431
|
+
resolvedPath,
|
|
411
432
|
loc: {start: spec.start, end: spec.end, path: myPath},
|
|
412
433
|
};
|
|
413
434
|
} else if (spec.type === "ImportSpecifier") {
|
|
@@ -418,6 +439,8 @@ const getLocals = (
|
|
|
418
439
|
? spec.imported.name
|
|
419
440
|
: spec.imported.value,
|
|
420
441
|
path: importPath,
|
|
442
|
+
rawPath: toplevel.source.value,
|
|
443
|
+
resolvedPath,
|
|
421
444
|
loc: {start: spec.start, end: spec.end, path: myPath},
|
|
422
445
|
};
|
|
423
446
|
}
|
|
@@ -449,7 +472,7 @@ export const processFiles = (
|
|
|
449
472
|
}
|
|
450
473
|
const result = processFile(next, source, config);
|
|
451
474
|
files[next] = result;
|
|
452
|
-
listExternalReferences(result
|
|
475
|
+
listExternalReferences(result).forEach((path) => {
|
|
453
476
|
if (!files[path] && !toProcess.includes(path)) {
|
|
454
477
|
toProcess.push(path);
|
|
455
478
|
}
|
package/src/parser/resolve.ts
CHANGED
|
@@ -51,7 +51,7 @@ const resolveImport = (
|
|
|
51
51
|
},
|
|
52
52
|
config: Config,
|
|
53
53
|
): Document | null | undefined => {
|
|
54
|
-
const absPath = getPathWithExtension(expr.path
|
|
54
|
+
const absPath = getPathWithExtension(expr.path);
|
|
55
55
|
if (!absPath) {
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
@@ -71,6 +71,23 @@ const resolveImport = (
|
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
73
|
if (!res.exports[expr.name]) {
|
|
74
|
+
if (expr.name !== "*" && res.exportAlls.length) {
|
|
75
|
+
for (const exportAll of res.exportAlls) {
|
|
76
|
+
const value = resolveImport(
|
|
77
|
+
{
|
|
78
|
+
...exportAll,
|
|
79
|
+
name: expr.name,
|
|
80
|
+
},
|
|
81
|
+
files,
|
|
82
|
+
errors,
|
|
83
|
+
{...seen},
|
|
84
|
+
config,
|
|
85
|
+
);
|
|
86
|
+
if (value) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
74
91
|
errors.push({
|
|
75
92
|
loc: expr.loc,
|
|
76
93
|
message: `${absPath} has no valid gql export ${expr.name}`,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copied from https://github.com/Khan/frontend/blob/main/libs/node/resolve-import/src/resolve-import.ts
|
|
2
|
+
// In future it would be cool to publish @khan/node-resolve-import as a public library so we
|
|
3
|
+
// could consume it here.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import {ResolverFactory} from "rspack-resolver";
|
|
7
|
+
import {createMatchPath, loadConfig} from "tsconfig-paths";
|
|
8
|
+
|
|
9
|
+
const CONDITION_NAMES = ["import"];
|
|
10
|
+
|
|
11
|
+
const matchPathCache = new Map<
|
|
12
|
+
string,
|
|
13
|
+
(importPath: string) => string | undefined
|
|
14
|
+
>();
|
|
15
|
+
|
|
16
|
+
let esmResolver = getEsmResolver();
|
|
17
|
+
|
|
18
|
+
function getEsmResolver() {
|
|
19
|
+
return new ResolverFactory({
|
|
20
|
+
conditionNames: CONDITION_NAMES,
|
|
21
|
+
mainFields: ["module", "main"],
|
|
22
|
+
extensions: [".mjs", ".js", ".jsx", ".ts", ".tsx"],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resetImportCache() {
|
|
27
|
+
matchPathCache.clear();
|
|
28
|
+
esmResolver = getEsmResolver();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolves an import path using tsconfig and the rspack resolver.
|
|
33
|
+
*
|
|
34
|
+
* @param sourceFile - The file that is importing the path.
|
|
35
|
+
* @param importPath - The path to resolve.
|
|
36
|
+
* @returns The fully resolved path.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveImportPath(sourceFile: string, importPath: string) {
|
|
39
|
+
const dir = path.dirname(sourceFile);
|
|
40
|
+
let matchPath = matchPathCache.get(dir);
|
|
41
|
+
|
|
42
|
+
if (!matchPath) {
|
|
43
|
+
const foundConfig = loadConfig(dir);
|
|
44
|
+
|
|
45
|
+
if (foundConfig.resultType !== "success") {
|
|
46
|
+
throw new Error("Failed to load tsconfig");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
matchPath = createMatchPath(
|
|
50
|
+
foundConfig.absoluteBaseUrl,
|
|
51
|
+
foundConfig.paths,
|
|
52
|
+
CONDITION_NAMES,
|
|
53
|
+
);
|
|
54
|
+
matchPathCache.set(dir, matchPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// See if we can resolve the import path using the tsconfig.
|
|
58
|
+
const resolvedPath = matchPath(importPath);
|
|
59
|
+
|
|
60
|
+
// Get the directory of the source file to resolve against.
|
|
61
|
+
const sourceFileDir = path.dirname(sourceFile);
|
|
62
|
+
|
|
63
|
+
// Get the fully resolved path.
|
|
64
|
+
return esmResolver.sync(sourceFileDir, resolvedPath ?? importPath).path;
|
|
65
|
+
}
|
package/src/parser/utils.ts
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import {Config} from "../types";
|
|
3
2
|
|
|
4
|
-
export const
|
|
5
|
-
if (config.alias) {
|
|
6
|
-
for (const {find, replacement} of config.alias) {
|
|
7
|
-
path = path.replace(find, replacement);
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
return path;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const getPathWithExtension = (
|
|
14
|
-
pathWithoutExtension: string,
|
|
15
|
-
config: Config,
|
|
16
|
-
) => {
|
|
17
|
-
pathWithoutExtension = fixPathResolution(pathWithoutExtension, config);
|
|
3
|
+
export const getPathWithExtension = (pathWithoutExtension: string) => {
|
|
18
4
|
if (
|
|
19
5
|
/\.(less|css|png|gif|jpg|jpeg|js|jsx|ts|tsx|mjs)$/.test(
|
|
20
6
|
pathWithoutExtension,
|
package/src/types.ts
CHANGED