uql-codemod 0.35.1 → 0.36.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/README.md +7 -2
- package/dist/edits.d.ts +5 -4
- package/dist/edits.js +10 -5
- package/dist/entitySql.d.ts +23 -0
- package/dist/entitySql.js +67 -0
- package/dist/keyMaps.d.ts +9 -6
- package/dist/keyMaps.js +29 -45
- package/dist/transform.js +356 -110
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,10 +26,12 @@ Only `--flag=value` is read, and anything unrecognised is an error. It exits `0`
|
|
|
26
26
|
|
|
27
27
|
- `type` on a `@Field`/`@Id` that has none, from the property's declared type; not beside `references`, whose column comes from the key it points at.
|
|
28
28
|
- `entity: () => X` on a bare relation decorator, and `Relation<T>` to `T`, its import going with the last use.
|
|
29
|
-
- `virtual` to `computed
|
|
29
|
+
- `virtual` to `computed` (on `@Field` and in `defineEntity`'s `fields`), a check's `expression` and a filter's `condition` to `where` (`@Entity`/`defineEntity`, `@Filter`, `defineFilter`), `raw('sql')` to the tagged template, and a `raw` alias argument onto `.as()`.
|
|
30
30
|
- Members named by string to key-map callbacks: `mappedBy: 'author'` to `(post) => post.author`, and `@Index`, `include`, `references` and `@Entity`/`defineEntity`'s `indexes` and `hooks` likewise: `@Index((post) => [post.title])`.
|
|
31
|
+
- An index expression to the callback an index takes, as an entry or a `column`: ``raw`lower("email")` `` to ``() => raw`lower("email")` ``, and a partial-index `where` string to `raw`, importing it; on entities and in the migration builder.
|
|
32
|
+
- `col('cost')` to the ref it names, ``(product) => raw`${product.cost}` `` in a `computed` and `refs(Item).cost` in a statement, and `$lock: { wait: 'skip' }` to `{ $wait: 'skip' }`, or to `true` for `{}` and a `'block'` wait.
|
|
31
33
|
- An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
|
|
32
|
-
- Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, and the removed driver classes to the one each extended: `PgDialect`/`NeonDialect`/`PgliteDialect` to `PostgresDialect`, `CrdbQuerier`/`NeonQuerier` to `PgQuerier`, `LibsqlQuerier`/`TursoQuerier` to `HranaQuerier`, `MySql2Dialect` to `MySqlDialect`, `MongodbNativeDialect` to `MongoDialect`, moving the import to the entry that exports it.
|
|
34
|
+
- Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, and the removed driver classes to the one each extended: `PgDialect`/`NeonDialect`/`PgliteDialect` to `PostgresDialect`, `CrdbQuerier`/`NeonQuerier` to `PgQuerier`, `LibsqlQuerier`/`TursoQuerier` to `HranaQuerier`, `MySql2Dialect` to `MySqlDialect`, `MongodbNativeDialect` to `MongoDialect`, moving the import to the entry that exports it.
|
|
33
35
|
- The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
|
|
34
36
|
- `import 'reflect-metadata'`, and `experimentalDecorators`/`emitDecoratorMetadata` in `tsconfig.json`, comments and formatting kept.
|
|
35
37
|
|
|
@@ -41,6 +43,9 @@ Reported with what to do, never guessed:
|
|
|
41
43
|
- `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
|
|
42
44
|
- An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...).
|
|
43
45
|
- A value it cannot read: options passed as a variable or a spread, a list that is not a literal, a key under a computed name.
|
|
46
|
+
- A partial-index `where` string that interpolates, since `raw` would bind what the template spliced in.
|
|
47
|
+
- A `col()` whose entity it cannot tell: a raw built apart from its query, one under a relation filter, or a column no field maps.
|
|
48
|
+
- SQL in a definition that names a column by hand, as a note: a rename reaches a callback's refs, ``(user) => raw`lower(${user.email})` ``, and never the text.
|
|
44
49
|
- A property whose type maps to no column (`Json<T>`, a vector, a mixed union).
|
|
45
50
|
- A branded string id, written as `type: String` to keep the column you have, with a note: `'uuid'` is a native column, and a migration.
|
|
46
51
|
|
package/dist/edits.d.ts
CHANGED
|
@@ -6,12 +6,13 @@ export type Edit = {
|
|
|
6
6
|
readonly text: string;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
* Applies edits back to front, so
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* that reformats everything it touches buries its own change in the diff.
|
|
9
|
+
* Applies edits back to front, so none shifts the offsets of another, and at a shared offset the replacement
|
|
10
|
+
* first, so an insertion there lands before it. Splicing rather than reprinting keeps the rest of the file
|
|
11
|
+
* byte for byte: a codemod that reformats everything it touches buries its own change in the diff.
|
|
13
12
|
*/
|
|
14
13
|
export declare function applyEdits(text: string, edits: readonly Edit[]): string;
|
|
14
|
+
export declare function replaced(node: ts.Node, text: string): Edit;
|
|
15
|
+
export declare function inserted(node: ts.Node, text: string): Edit;
|
|
15
16
|
/**
|
|
16
17
|
* Removes one element of a comma-separated list - a parameter, a named import, a JSON property - taking
|
|
17
18
|
* the separator that would otherwise dangle. The last element gives up the comma before it; any other
|
package/dist/edits.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Applies edits back to front, so
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* that reformats everything it touches buries its own change in the diff.
|
|
2
|
+
* Applies edits back to front, so none shifts the offsets of another, and at a shared offset the replacement
|
|
3
|
+
* first, so an insertion there lands before it. Splicing rather than reprinting keeps the rest of the file
|
|
4
|
+
* byte for byte: a codemod that reformats everything it touches buries its own change in the diff.
|
|
6
5
|
*/
|
|
7
6
|
export function applyEdits(text, edits) {
|
|
8
7
|
let result = text;
|
|
9
|
-
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
8
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start || b.end - a.end)) {
|
|
10
9
|
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
11
10
|
}
|
|
12
11
|
return result;
|
|
13
12
|
}
|
|
13
|
+
export function replaced(node, text) {
|
|
14
|
+
return { start: node.getStart(), end: node.getEnd(), text };
|
|
15
|
+
}
|
|
16
|
+
export function inserted(node, text) {
|
|
17
|
+
return { start: node.getStart(), end: node.getStart(), text };
|
|
18
|
+
}
|
|
14
19
|
/**
|
|
15
20
|
* Removes one element of a comma-separated list - a parameter, a named import, a JSON property - taking
|
|
16
21
|
* the separator that would otherwise dangle. The last element gives up the comma before it; any other
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { type Edit } from './edits.js';
|
|
3
|
+
/** `raw\`...\`` or `raw(...)`. */
|
|
4
|
+
type RawSql = ts.TaggedTemplateExpression | ts.CallExpression;
|
|
5
|
+
export declare function isRaw(node: ts.Node): node is RawSql;
|
|
6
|
+
/**
|
|
7
|
+
* `() => ` before each expression a column list indexes, which an index takes as a callback now. One that
|
|
8
|
+
* interpolates takes the list callback's parameter as its refs: `(user) => [raw\`lower(${user.email})\`]`
|
|
9
|
+
* spliced in the key `'email'`, where the ref it reads now renders the column.
|
|
10
|
+
*/
|
|
11
|
+
export declare function expressionEntryEdits(list: ts.Expression | undefined): readonly Edit[];
|
|
12
|
+
/** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
|
|
13
|
+
export declare function columnExpressions(list: ts.Expression | undefined): readonly RawSql[];
|
|
14
|
+
/** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
|
|
15
|
+
export declare function rawTag(sql: string): string;
|
|
16
|
+
/** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
|
|
17
|
+
export declare function rawWhereEdit(where: ts.Expression): Edit | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
|
|
20
|
+
* naming one of `members`. What a string literal holds is left out, as is a word called as a function.
|
|
21
|
+
*/
|
|
22
|
+
export declare function handNamedColumns(sql: ts.Expression, members: ReadonlySet<string>): readonly string[];
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { inserted, replaced } from './edits.js';
|
|
3
|
+
import { propertyValue } from './keyMaps.js';
|
|
4
|
+
export function isRaw(node) {
|
|
5
|
+
const callee = ts.isTaggedTemplateExpression(node)
|
|
6
|
+
? node.tag
|
|
7
|
+
: ts.isCallExpression(node)
|
|
8
|
+
? node.expression
|
|
9
|
+
: undefined;
|
|
10
|
+
return callee !== undefined && ts.isIdentifier(callee) && callee.text === 'raw';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `() => ` before each expression a column list indexes, which an index takes as a callback now. One that
|
|
14
|
+
* interpolates takes the list callback's parameter as its refs: `(user) => [raw\`lower(${user.email})\`]`
|
|
15
|
+
* spliced in the key `'email'`, where the ref it reads now renders the column.
|
|
16
|
+
*/
|
|
17
|
+
export function expressionEntryEdits(list) {
|
|
18
|
+
const param = list && ts.isArrowFunction(list) ? list.parameters[0]?.name : undefined;
|
|
19
|
+
const refs = param && ts.isIdentifier(param) ? param.text : '';
|
|
20
|
+
return columnExpressions(list).map((sql) => inserted(sql, `(${interpolates(sql) ? refs : ''}) => `));
|
|
21
|
+
}
|
|
22
|
+
/** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
|
|
23
|
+
export function columnExpressions(list) {
|
|
24
|
+
const body = list && ts.isArrowFunction(list) ? list.body : list;
|
|
25
|
+
const entries = body && ts.isArrayLiteralExpression(body) ? body.elements : [];
|
|
26
|
+
return entries.flatMap((entry) => {
|
|
27
|
+
const sql = ts.isObjectLiteralExpression(entry) ? propertyValue(entry, 'column') : entry;
|
|
28
|
+
return sql && isRaw(sql) ? [sql] : [];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function interpolates(sql) {
|
|
32
|
+
return ts.isTaggedTemplateExpression(sql) && ts.isTemplateExpression(sql.template);
|
|
33
|
+
}
|
|
34
|
+
/** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
|
|
35
|
+
export function rawTag(sql) {
|
|
36
|
+
return `raw\`${sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\``;
|
|
37
|
+
}
|
|
38
|
+
/** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
|
|
39
|
+
export function rawWhereEdit(where) {
|
|
40
|
+
if (ts.isNoSubstitutionTemplateLiteral(where)) {
|
|
41
|
+
return inserted(where, 'raw');
|
|
42
|
+
}
|
|
43
|
+
return ts.isStringLiteral(where) ? replaced(where, rawTag(where.text)) : undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
|
|
47
|
+
* naming one of `members`. What a string literal holds is left out, as is a word called as a function.
|
|
48
|
+
*/
|
|
49
|
+
export function handNamedColumns(sql, members) {
|
|
50
|
+
const code = sqlText(sql)?.replace(/'(?:[^']|'')*'/g, "''") ?? '';
|
|
51
|
+
const quoted = Array.from(code.matchAll(/"[^"]+"|`[^`]+`/g), ([token]) => token.slice(1, -1));
|
|
52
|
+
const bare = Array.from(code.matchAll(/\b[A-Za-z_]\w*\b(?!\s*\()/g), ([word]) => word);
|
|
53
|
+
return [...new Set([...quoted, ...bare.filter((word) => members.has(word))])];
|
|
54
|
+
}
|
|
55
|
+
/** The text of a `raw` or a string, `?` standing in for each interpolation. */
|
|
56
|
+
function sqlText(sql) {
|
|
57
|
+
if (!sql) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (isRaw(sql)) {
|
|
61
|
+
return sqlText(ts.isTaggedTemplateExpression(sql) ? sql.template : sql.arguments[0]);
|
|
62
|
+
}
|
|
63
|
+
if (ts.isTemplateExpression(sql)) {
|
|
64
|
+
return [sql.head, ...sql.templateSpans.map((span) => span.literal)].map((part) => part.text).join(' ? ');
|
|
65
|
+
}
|
|
66
|
+
return ts.isStringLiteralLike(sql) ? sql.text : undefined;
|
|
67
|
+
}
|
package/dist/keyMaps.d.ts
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import type
|
|
2
|
+
import { type Edit } from './edits.js';
|
|
3
|
+
/** The edits of one rewrite, or `undefined` where the value it reads could not be read. */
|
|
4
|
+
export type Edits = readonly Edit[] | undefined;
|
|
3
5
|
/** A callback's parameter, named after the class it reads: `Post` -> `post`. */
|
|
4
6
|
export declare function paramFor(className: string | undefined): string;
|
|
7
|
+
/** `post.title`, or `post['first-name']` for a key that is no identifier. */
|
|
8
|
+
export declare function memberAccess(param: string, key: string): string;
|
|
5
9
|
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
6
10
|
export declare function propertyKey(name: ts.PropertyName): string | undefined;
|
|
7
11
|
/** The initializer of the property `name` in an object literal, however its key is written. */
|
|
8
12
|
export declare function propertyValue(node: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined;
|
|
9
13
|
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
10
|
-
export declare function
|
|
14
|
+
export declare function fieldKeysEdits(value: ts.Expression): Edits;
|
|
11
15
|
/** Whether `value` is already a function, the form every rewrite here produces. */
|
|
12
16
|
export declare function isCallback(value: ts.Expression): boolean;
|
|
13
17
|
/** `['title', { column: 'createdAt' }]` -> `(post) => [post.title, { column: post.createdAt }]`. */
|
|
14
|
-
export declare function
|
|
18
|
+
export declare function keyListEdits(list: ts.Expression, param: string): Edits;
|
|
15
19
|
/** `'author'` -> `(post) => post.author`, `param` being the relation's target. */
|
|
16
|
-
export declare function
|
|
20
|
+
export declare function mappedByEdits(value: ts.Expression, param: string): Edits;
|
|
17
21
|
/**
|
|
18
22
|
* `[{ local: 'customerCode', foreign: 'code' }]` -> `(order, customer) => [{ local: order.customerCode,
|
|
19
23
|
* foreign: customer.code }]`. A self-relation would give both parameters one name, so it reads
|
|
20
24
|
* `(local, foreign)` instead.
|
|
21
25
|
*/
|
|
22
|
-
export declare function
|
|
26
|
+
export declare function referencesEdits(value: ts.Expression, owner: string, target: string): Edits;
|
|
23
27
|
/** The class an `entity: () => Post` getter returns, as written. */
|
|
24
28
|
export declare function entityGetterTarget(relation: ts.ObjectLiteralExpression): string | undefined;
|
|
25
|
-
export declare function replaced(node: ts.Node, text: string): Edit;
|
package/dist/keyMaps.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
+
import { inserted, replaced } from './edits.js';
|
|
2
3
|
/** A callback's parameter, named after the class it reads: `Post` -> `post`. */
|
|
3
4
|
export function paramFor(className) {
|
|
4
5
|
return className ? className[0].toLowerCase() + className.slice(1) : 'entity';
|
|
@@ -7,7 +8,7 @@ function isIdentifierName(key) {
|
|
|
7
8
|
return /^[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*$/u.test(key);
|
|
8
9
|
}
|
|
9
10
|
/** `post.title`, or `post['first-name']` for a key that is no identifier. */
|
|
10
|
-
function memberAccess(param, key) {
|
|
11
|
+
export function memberAccess(param, key) {
|
|
11
12
|
return isIdentifierName(key) ? `${param}.${key}` : `${param}['${key}']`;
|
|
12
13
|
}
|
|
13
14
|
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
@@ -20,93 +21,76 @@ export function propertyValue(node, name) {
|
|
|
20
21
|
return property?.initializer;
|
|
21
22
|
}
|
|
22
23
|
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
23
|
-
export function
|
|
24
|
+
export function fieldKeysEdits(value) {
|
|
24
25
|
const names = ts.isArrayLiteralExpression(value) ? value.elements : [value];
|
|
25
26
|
if (!names.every((name) => ts.isStringLiteralLike(name))) {
|
|
26
27
|
return undefined;
|
|
27
28
|
}
|
|
28
29
|
const keys = names.map(({ text }) => `${isIdentifierName(text) ? text : `'${text}'`}: true`);
|
|
29
|
-
return replaced(value, `{ ${keys.join(', ')} }`);
|
|
30
|
+
return [replaced(value, `{ ${keys.join(', ')} }`)];
|
|
30
31
|
}
|
|
31
32
|
/** Whether `value` is already a function, the form every rewrite here produces. */
|
|
32
33
|
export function isCallback(value) {
|
|
33
34
|
return ts.isArrowFunction(value) || ts.isFunctionExpression(value);
|
|
34
35
|
}
|
|
35
36
|
/** `['title', { column: 'createdAt' }]` -> `(post) => [post.title, { column: post.createdAt }]`. */
|
|
36
|
-
export function
|
|
37
|
-
|
|
38
|
-
?
|
|
37
|
+
export function keyListEdits(list, param) {
|
|
38
|
+
return ts.isArrayLiteralExpression(list)
|
|
39
|
+
? callbackEdits(list, param, list.elements.map((entry) => keyListEntryEdits(entry, param)))
|
|
39
40
|
: undefined;
|
|
40
|
-
return callbackEdit(list, param, text);
|
|
41
41
|
}
|
|
42
42
|
/** `'author'` -> `(post) => post.author`, `param` being the relation's target. */
|
|
43
|
-
export function
|
|
44
|
-
return
|
|
43
|
+
export function mappedByEdits(value, param) {
|
|
44
|
+
return ts.isStringLiteralLike(value)
|
|
45
|
+
? [replaced(value, `(${param}) => ${memberAccess(param, value.text)}`)]
|
|
46
|
+
: undefined;
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
47
49
|
* `[{ local: 'customerCode', foreign: 'code' }]` -> `(order, customer) => [{ local: order.customerCode,
|
|
48
50
|
* foreign: customer.code }]`. A self-relation would give both parameters one name, so it reads
|
|
49
51
|
* `(local, foreign)` instead.
|
|
50
52
|
*/
|
|
51
|
-
export function
|
|
53
|
+
export function referencesEdits(value, owner, target) {
|
|
52
54
|
const [local, foreign] = owner === target ? ['local', 'foreign'] : [owner, target];
|
|
53
|
-
|
|
54
|
-
?
|
|
55
|
+
return ts.isArrayLiteralExpression(value)
|
|
56
|
+
? callbackEdits(value, `${local}, ${foreign}`, value.elements.map((pair) => referenceEdits(pair, local, foreign)))
|
|
55
57
|
: undefined;
|
|
56
|
-
return callbackEdit(value, `${local}, ${foreign}`, text);
|
|
57
58
|
}
|
|
58
59
|
/** The class an `entity: () => Post` getter returns, as written. */
|
|
59
60
|
export function entityGetterTarget(relation) {
|
|
60
61
|
const getter = propertyValue(relation, 'entity');
|
|
61
62
|
return getter && ts.isArrowFunction(getter) && ts.isIdentifier(getter.body) ? getter.body.text : undefined;
|
|
62
63
|
}
|
|
63
|
-
/**
|
|
64
|
-
function
|
|
64
|
+
/** A column-list entry, its name read off the key map: an expression, or a `column` given otherwise, has none. */
|
|
65
|
+
function keyListEntryEdits(entry, param) {
|
|
65
66
|
if (ts.isStringLiteralLike(entry)) {
|
|
66
|
-
return memberAccess(param, entry.text);
|
|
67
|
+
return [replaced(entry, memberAccess(param, entry.text))];
|
|
67
68
|
}
|
|
68
69
|
if (ts.isTaggedTemplateExpression(entry) || ts.isCallExpression(entry)) {
|
|
69
|
-
return
|
|
70
|
+
return [];
|
|
70
71
|
}
|
|
71
72
|
if (!ts.isObjectLiteralExpression(entry)) {
|
|
72
73
|
return undefined;
|
|
73
74
|
}
|
|
74
75
|
const column = propertyValue(entry, 'column');
|
|
75
|
-
return column && ts.isStringLiteralLike(column)
|
|
76
|
-
? spliced(entry, [[column, memberAccess(param, column.text)]])
|
|
77
|
-
: entry.getText();
|
|
76
|
+
return column && ts.isStringLiteralLike(column) ? [replaced(column, memberAccess(param, column.text))] : [];
|
|
78
77
|
}
|
|
79
|
-
function
|
|
78
|
+
function referenceEdits(pair, local, foreign) {
|
|
80
79
|
if (!ts.isObjectLiteralExpression(pair)) {
|
|
81
80
|
return undefined;
|
|
82
81
|
}
|
|
83
82
|
const localKey = propertyValue(pair, 'local');
|
|
84
83
|
const foreignKey = propertyValue(pair, 'foreign');
|
|
85
84
|
return localKey && foreignKey && ts.isStringLiteralLike(localKey) && ts.isStringLiteralLike(foreignKey)
|
|
86
|
-
?
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
]
|
|
85
|
+
? [
|
|
86
|
+
replaced(localKey, memberAccess(local, localKey.text)),
|
|
87
|
+
replaced(foreignKey, memberAccess(foreign, foreignKey.text)),
|
|
88
|
+
]
|
|
90
89
|
: undefined;
|
|
91
90
|
}
|
|
92
|
-
/** `
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const start = node.getStart();
|
|
98
|
-
const source = node.getText();
|
|
99
|
-
const sorted = [...parts].sort(([a], [b]) => a.getStart() - b.getStart());
|
|
100
|
-
const { text, at } = sorted.reduce((acc, [part, replacement]) => ({
|
|
101
|
-
text: acc.text + source.slice(acc.at, part.getStart() - start) + replacement,
|
|
102
|
-
at: part.getEnd() - start,
|
|
103
|
-
}), { text: '', at: 0 });
|
|
104
|
-
return text + source.slice(at);
|
|
105
|
-
}
|
|
106
|
-
/** `value` replaced by `(params) => body`, or nothing when the body could not be read. */
|
|
107
|
-
function callbackEdit(value, params, body) {
|
|
108
|
-
return body === undefined ? undefined : replaced(value, `(${params}) => ${body}`);
|
|
109
|
-
}
|
|
110
|
-
export function replaced(node, text) {
|
|
111
|
-
return { start: node.getStart(), end: node.getEnd(), text };
|
|
91
|
+
/** `(params) => ` before `body`, with the edits of its parts, or nothing where one could not be read. */
|
|
92
|
+
function callbackEdits(body, params, parts) {
|
|
93
|
+
return parts.every((part) => part !== undefined)
|
|
94
|
+
? [inserted(body, `(${params}) => `), ...parts.flat()]
|
|
95
|
+
: undefined;
|
|
112
96
|
}
|
package/dist/transform.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import { applyEdits, removeFromList } from './edits.js';
|
|
2
|
+
import { applyEdits, inserted, removeFromList, replaced } from './edits.js';
|
|
3
|
+
import { columnExpressions, expressionEntryEdits, handNamedColumns, isRaw, rawTag, rawWhereEdit } from './entitySql.js';
|
|
3
4
|
import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
|
|
4
|
-
import { entityGetterTarget,
|
|
5
|
+
import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, propertyKey, propertyValue, referencesEdits, } from './keyMaps.js';
|
|
5
6
|
const FIELD_DECORATORS = new Set(['Field', 'Id']);
|
|
6
7
|
const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
|
|
7
8
|
/**
|
|
@@ -38,6 +39,7 @@ const REMOVED_EXPORTS = new Map([
|
|
|
38
39
|
const RENAMED_EXPORTS = new Map([
|
|
39
40
|
['QueryWhereMap', { to: 'QueryWhere' }],
|
|
40
41
|
['RelationKeyMap', { to: 'KeyMap' }],
|
|
42
|
+
['FilterCondition', { to: 'FilterWhere' }],
|
|
41
43
|
['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
42
44
|
['NeonDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
43
45
|
['PgliteDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
@@ -64,18 +66,19 @@ function decoratorOptions(node) {
|
|
|
64
66
|
return { kind: 'opaque', reason: 'it is used without being called' };
|
|
65
67
|
}
|
|
66
68
|
const [first] = node.expression.arguments;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
return first ? optionsOf(first) : { kind: 'empty', call: node.expression };
|
|
70
|
+
}
|
|
71
|
+
/** The options `value` writes: a literal to read and write into, or why it is not one. */
|
|
72
|
+
function optionsOf(value) {
|
|
73
|
+
if (!ts.isObjectLiteralExpression(value)) {
|
|
74
|
+
return { kind: 'opaque', reason: `its options are passed as '${value.getText()}'` };
|
|
72
75
|
}
|
|
73
|
-
return
|
|
76
|
+
return value.properties.some(ts.isSpreadAssignment)
|
|
74
77
|
? { kind: 'opaque', reason: 'its options object spreads another' }
|
|
75
|
-
: { kind: 'literal', node:
|
|
78
|
+
: { kind: 'literal', node: value };
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
|
-
* The property `name` in the
|
|
81
|
+
* The property `name` in the options, whether its key is plain, quoted or a shorthand.
|
|
79
82
|
*
|
|
80
83
|
* All three forms, because both callers care about the option being *there*: one to leave an option
|
|
81
84
|
* alone that is already stated, the other to rename it. Matching only the plain key inserted a second
|
|
@@ -88,27 +91,44 @@ function findProperty(options, name) {
|
|
|
88
91
|
return options.node.properties.find((prop) => (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && propertyKey(prop.name) === name);
|
|
89
92
|
}
|
|
90
93
|
/**
|
|
91
|
-
* Renames the `
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* object survive untouched. A shorthand has no value to keep, so it becomes `computed: virtual` -
|
|
95
|
-
* renaming the key alone would rebind it to a local that does not exist.
|
|
94
|
+
* Renames the option `from` to `to`. Only the key is replaced, so the value, its formatting and any comment
|
|
95
|
+
* inside survive; a shorthand has no value to keep, so it becomes `to: from`, since renaming the key alone
|
|
96
|
+
* would rebind it to a local that does not exist.
|
|
96
97
|
*/
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
98
|
+
function renameOption(options, from, to, node, ctx) {
|
|
99
|
+
const property = findProperty(options, from);
|
|
100
|
+
if (!property) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (findProperty(options, to)) {
|
|
104
|
+
ctx.unresolved.push(`${ctx.describe(node)}: gives both '${from}' and '${to}'; keep '${to}'.`);
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
ctx.edits.push(replaced(property.name, ts.isShorthandPropertyAssignment(property) ? `${to}: ${from}` : to));
|
|
108
|
+
}
|
|
109
|
+
/** {@link renameOption} for an option holding SQL - a field's `virtual`, a check's `expression` - noting that SQL. */
|
|
110
|
+
function renameSqlOption(options, from, to, node, owner, ctx) {
|
|
111
|
+
renameOption(options, from, to, node, ctx);
|
|
112
|
+
if (options.kind === 'literal') {
|
|
113
|
+
noteHandNamedColumns(propertyValue(options.node, from) ?? propertyValue(options.node, to), owner, ctx);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Notes SQL that names a column by hand, which a rename does not reach. */
|
|
117
|
+
function noteHandNamedColumns(sql, owner, ctx) {
|
|
118
|
+
const names = sql ? handNamedColumns(sql, memberNames(owner.entity, ctx.checker)) : [];
|
|
119
|
+
if (!sql || !names.length) {
|
|
108
120
|
return;
|
|
109
121
|
}
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
ctx.notes.push(`${ctx.describe(sql)}: SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename ` +
|
|
123
|
+
`does not reach; read each off a callback's refs instead: (${owner.param}) => raw\`...\${${owner.param}.<member>}...\``);
|
|
124
|
+
}
|
|
125
|
+
/** The names of the members of the entity `node` declares or names. */
|
|
126
|
+
function memberNames(node, checker) {
|
|
127
|
+
const type = checker.getTypeAtLocation(node);
|
|
128
|
+
return propertyNames(type.getConstructSignatures()[0]?.getReturnType() ?? type);
|
|
129
|
+
}
|
|
130
|
+
function propertyNames(type) {
|
|
131
|
+
return new Set(type.getProperties().map(({ name }) => name));
|
|
112
132
|
}
|
|
113
133
|
/** Inserts `option` into the decorator's options object, creating one when the call has no arguments. */
|
|
114
134
|
function insertOption(options, option) {
|
|
@@ -118,9 +138,7 @@ function insertOption(options, option) {
|
|
|
118
138
|
return { start: args.pos, end: args.end, text: `{ ${option} }` };
|
|
119
139
|
}
|
|
120
140
|
const first = options.node.properties[0];
|
|
121
|
-
return first
|
|
122
|
-
? { start: first.getStart(), end: first.getStart(), text: `${option}, ` }
|
|
123
|
-
: { start: options.node.getStart(), end: options.node.getEnd(), text: `{ ${option} }` };
|
|
141
|
+
return first ? inserted(first, `${option}, `) : replaced(options.node, `{ ${option} }`);
|
|
124
142
|
}
|
|
125
143
|
/**
|
|
126
144
|
* Writes one option the decorator can no longer infer at runtime, or records why it could not.
|
|
@@ -177,7 +195,7 @@ function addRelationEntity(decorator, node, ctx) {
|
|
|
177
195
|
});
|
|
178
196
|
}
|
|
179
197
|
/**
|
|
180
|
-
* Writes the
|
|
198
|
+
* Writes the edits `rewrite` reads off `value`, which still names members as strings, or reports it
|
|
181
199
|
* where it could not be read: what it holds is only known at runtime. A callback or an object literal
|
|
182
200
|
* is already the new form, and kept.
|
|
183
201
|
*/
|
|
@@ -185,23 +203,23 @@ function rewriteValue(value, rewrite, node, advice, ctx) {
|
|
|
185
203
|
if (!value || isCallback(value) || ts.isObjectLiteralExpression(value)) {
|
|
186
204
|
return;
|
|
187
205
|
}
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
ctx.edits.push(
|
|
206
|
+
const edits = rewrite(value);
|
|
207
|
+
if (edits) {
|
|
208
|
+
ctx.edits.push(...edits);
|
|
191
209
|
}
|
|
192
210
|
else {
|
|
193
211
|
ctx.unresolved.push(`${ctx.describe(node)}: ${advice}; this one could not be read`);
|
|
194
212
|
}
|
|
195
213
|
}
|
|
196
214
|
function rewriteKeyList(value, param, node, what, ctx) {
|
|
197
|
-
rewriteValue(value, (list) =>
|
|
215
|
+
rewriteValue(value, (list) => keyListEdits(list, param), node, `write ${what} as a key-map callback`, ctx);
|
|
198
216
|
}
|
|
199
217
|
/**
|
|
200
|
-
* Rewrites
|
|
201
|
-
*
|
|
218
|
+
* Rewrites a class's decorators - `@Index`, `@Filter` and `@Entity`'s options - for what they define now:
|
|
219
|
+
* members named by string read off key maps named after the class, and SQL written as it is taken.
|
|
202
220
|
*/
|
|
203
221
|
function rewriteClassDecorators(node, ctx) {
|
|
204
|
-
const
|
|
222
|
+
const owner = { param: paramFor(node.name?.text), entity: node };
|
|
205
223
|
for (const decorator of decoratorsOf(node)) {
|
|
206
224
|
if (!ts.isCallExpression(decorator.expression)) {
|
|
207
225
|
continue;
|
|
@@ -209,54 +227,118 @@ function rewriteClassDecorators(node, ctx) {
|
|
|
209
227
|
const [first, second] = decorator.expression.arguments;
|
|
210
228
|
const name = decoratorName(decorator);
|
|
211
229
|
if (name === 'Index') {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
rewriteIndex(first, second, owner, decorator, "'@Index' columns", ctx);
|
|
231
|
+
}
|
|
232
|
+
else if (name === 'Filter' && second) {
|
|
233
|
+
renameOption(optionsOf(second), 'condition', 'where', decorator, ctx);
|
|
216
234
|
}
|
|
217
235
|
else if (name === 'Entity' && first && ts.isObjectLiteralExpression(first)) {
|
|
218
|
-
rewriteEntityOptions(first,
|
|
236
|
+
rewriteEntityOptions(first, owner, ctx);
|
|
219
237
|
}
|
|
220
238
|
}
|
|
221
239
|
}
|
|
222
240
|
/**
|
|
223
|
-
* Rewrites
|
|
224
|
-
* `
|
|
225
|
-
*
|
|
241
|
+
* Rewrites the imperative API the same way: `defineEntity(Post, { ... })`, `defineIndex(Post, { columns })`,
|
|
242
|
+
* `defineRelation(Post, 'tag', { mappedBy, references })` and `defineFilter(Post, 'live', { condition })`.
|
|
243
|
+
* Only uql's shapes, a class and a literal options object, are read.
|
|
226
244
|
*/
|
|
227
245
|
function rewriteDefineCall(call, ctx) {
|
|
228
246
|
const name = ts.isIdentifier(call.expression) ? call.expression.text : undefined;
|
|
229
|
-
const [entity, second, third] = call.arguments;
|
|
230
|
-
const
|
|
247
|
+
const [entity = call, second, third] = call.arguments;
|
|
248
|
+
const owner = { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
|
|
231
249
|
if (name === 'defineEntity' && second && ts.isObjectLiteralExpression(second)) {
|
|
232
|
-
rewriteEntityOptions(second,
|
|
250
|
+
rewriteEntityOptions(second, owner, ctx);
|
|
233
251
|
}
|
|
234
252
|
else if (name === 'defineIndex' && second && ts.isObjectLiteralExpression(second)) {
|
|
235
|
-
rewriteIndexOptions(second,
|
|
253
|
+
rewriteIndexOptions(second, owner, ctx);
|
|
236
254
|
}
|
|
237
255
|
else if (name === 'defineRelation' && third && ts.isObjectLiteralExpression(third)) {
|
|
238
|
-
rewriteRelationOptions(third, param, ctx);
|
|
256
|
+
rewriteRelationOptions(third, owner.param, ctx);
|
|
257
|
+
}
|
|
258
|
+
else if (name === 'defineFilter' && third) {
|
|
259
|
+
renameOption(optionsOf(third), 'condition', 'where', call, ctx);
|
|
239
260
|
}
|
|
240
261
|
}
|
|
241
|
-
function rewriteEntityOptions(options,
|
|
242
|
-
const
|
|
243
|
-
for (const index of indexes && ts.isArrayLiteralExpression(indexes) ? indexes.elements : []) {
|
|
262
|
+
function rewriteEntityOptions(options, owner, ctx) {
|
|
263
|
+
for (const index of arrayElements(propertyValue(options, 'indexes'))) {
|
|
244
264
|
if (ts.isObjectLiteralExpression(index)) {
|
|
245
|
-
rewriteIndexOptions(index,
|
|
265
|
+
rewriteIndexOptions(index, owner, ctx);
|
|
246
266
|
}
|
|
247
267
|
}
|
|
268
|
+
for (const check of arrayElements(propertyValue(options, 'checks'))) {
|
|
269
|
+
renameSqlOption(optionsOf(check), 'expression', 'where', check, owner, ctx);
|
|
270
|
+
}
|
|
271
|
+
for (const field of objectProperties(propertyValue(options, 'fields'))) {
|
|
272
|
+
renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
|
|
273
|
+
}
|
|
274
|
+
for (const filter of objectProperties(propertyValue(options, 'filters'))) {
|
|
275
|
+
renameOption(optionsOf(filter.initializer), 'condition', 'where', filter, ctx);
|
|
276
|
+
}
|
|
248
277
|
for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
|
|
249
|
-
rewriteKeyList(hook.initializer, param, hook, 'a hook list', ctx);
|
|
278
|
+
rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
|
|
250
279
|
}
|
|
251
280
|
for (const relation of objectProperties(propertyValue(options, 'relations'))) {
|
|
252
281
|
if (ts.isObjectLiteralExpression(relation.initializer)) {
|
|
253
|
-
rewriteRelationOptions(relation.initializer, param, ctx);
|
|
282
|
+
rewriteRelationOptions(relation.initializer, owner.param, ctx);
|
|
254
283
|
}
|
|
255
284
|
}
|
|
256
285
|
}
|
|
257
|
-
function rewriteIndexOptions(index,
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
function rewriteIndexOptions(index, owner, ctx) {
|
|
287
|
+
rewriteIndex(propertyValue(index, 'columns'), index, owner, index, "'columns'", ctx);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* An index, as `@Index(columns, options)` or a `defineIndex`/`indexes` entry holding both: its columns and
|
|
291
|
+
* `include` read off the key map, its SQL written as {@link rewriteIndexSql} writes it, and noted.
|
|
292
|
+
*/
|
|
293
|
+
function rewriteIndex(columns, options, owner, node, what, ctx) {
|
|
294
|
+
const literal = options && ts.isObjectLiteralExpression(options) ? options : undefined;
|
|
295
|
+
rewriteKeyList(columns, owner.param, node, what, ctx);
|
|
296
|
+
rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
|
|
297
|
+
rewriteIndexSql(columns, literal, ctx);
|
|
298
|
+
for (const sql of [...columnExpressions(columns), literal && propertyValue(literal, 'where')]) {
|
|
299
|
+
noteHandNamedColumns(sql, owner, ctx);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* An index's SQL as an index takes it now, on an entity and in the migration builder alike: each expression
|
|
304
|
+
* a callback, and a `where` string `raw`. A string it cannot write as `raw` is reported.
|
|
305
|
+
*/
|
|
306
|
+
function rewriteIndexSql(columns, options, ctx) {
|
|
307
|
+
ctx.edits.push(...expressionEntryEdits(columns));
|
|
308
|
+
const where = options && propertyValue(options, 'where');
|
|
309
|
+
if (!where || !isStringTyped(ctx.checker.getTypeAtLocation(where))) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const edit = rawWhereEdit(where);
|
|
313
|
+
if (edit) {
|
|
314
|
+
ctx.edits.push(edit);
|
|
315
|
+
ctx.imports.set('raw', 'the raw`...`');
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
ctx.unresolved.push(`${ctx.describe(where)}: write the partial-index 'where' as raw\`...\` or a predicate; this one could not be read`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** Whether every value `type` admits is a string. */
|
|
322
|
+
function isStringTyped(type) {
|
|
323
|
+
return type.isUnion() ? type.types.every(isStringTyped) : Boolean(type.flags & ts.TypeFlags.StringLike);
|
|
324
|
+
}
|
|
325
|
+
/** The migration builder's index methods, each with the position of the columns it takes. */
|
|
326
|
+
const BUILDER_INDEX_METHODS = new Map([
|
|
327
|
+
['index', 0],
|
|
328
|
+
['unique', 0],
|
|
329
|
+
['addIndex', 0],
|
|
330
|
+
['createIndex', 1],
|
|
331
|
+
]);
|
|
332
|
+
/** Rewrites the SQL of the migration builder's `t.index()`, `t.unique()`, `t.addIndex()` and `m.createIndex()`. */
|
|
333
|
+
function rewriteBuilderCall(call, ctx) {
|
|
334
|
+
const at = ts.isPropertyAccessExpression(call.expression)
|
|
335
|
+
? BUILDER_INDEX_METHODS.get(call.expression.name.text)
|
|
336
|
+
: undefined;
|
|
337
|
+
if (at === undefined) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const options = call.arguments[at + 1];
|
|
341
|
+
rewriteIndexSql(call.arguments[at], options && ts.isObjectLiteralExpression(options) ? options : undefined, ctx);
|
|
260
342
|
}
|
|
261
343
|
/**
|
|
262
344
|
* `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
|
|
@@ -264,15 +346,15 @@ function rewriteIndexOptions(index, param, ctx) {
|
|
|
264
346
|
*/
|
|
265
347
|
function rewriteRelationOptions(relation, owner, ctx, target = paramFor(entityGetterTarget(relation))) {
|
|
266
348
|
const advice = (what) => `write '${what}' as a key-map callback`;
|
|
267
|
-
const mappedBy = (value) =>
|
|
268
|
-
const references = (value) =>
|
|
349
|
+
const mappedBy = (value) => mappedByEdits(value, target);
|
|
350
|
+
const references = (value) => referencesEdits(value, owner, target);
|
|
269
351
|
rewriteValue(propertyValue(relation, 'mappedBy'), mappedBy, relation, advice('mappedBy'), ctx);
|
|
270
352
|
rewriteValue(propertyValue(relation, 'references'), references, relation, advice('references'), ctx);
|
|
271
353
|
}
|
|
272
354
|
/**
|
|
273
|
-
* Rewrites
|
|
274
|
-
*
|
|
275
|
-
* `
|
|
355
|
+
* Rewrites a statement's keys: an aggregate's `$agg: { total: { $sum: 'amount' } }` into
|
|
356
|
+
* `$select: { total: { $sum: { amount: true } } }`, `$text`'s `$fields: ['title']` into `{ title: true }`, and
|
|
357
|
+
* `$lock`'s `wait` into `$wait`. Each key is uql's own, so it is read wherever a literal holds it.
|
|
276
358
|
*/
|
|
277
359
|
function rewriteStatementKeys(node, ctx) {
|
|
278
360
|
const key = propertyKey(node.name);
|
|
@@ -280,7 +362,21 @@ function rewriteStatementKeys(node, ctx) {
|
|
|
280
362
|
rewriteAggregate(node, ctx);
|
|
281
363
|
}
|
|
282
364
|
else if (key === '$fields' && isTextSearch(node.parent)) {
|
|
283
|
-
rewriteValue(node.initializer,
|
|
365
|
+
rewriteValue(node.initializer, fieldKeysEdits, node, "write '$fields' as { field: true }", ctx);
|
|
366
|
+
}
|
|
367
|
+
else if (key === '$lock' && ts.isObjectLiteralExpression(node.initializer)) {
|
|
368
|
+
rewriteLock(node.initializer, ctx);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** `{ wait: 'skip' }` as `{ $wait: 'skip' }`, and `{}` or a `'block'` wait as `true`, which waits. */
|
|
372
|
+
function rewriteLock(lock, ctx) {
|
|
373
|
+
const wait = propertyValue(lock, 'wait');
|
|
374
|
+
const blocks = wait && ts.isStringLiteralLike(wait) && wait.text === 'block';
|
|
375
|
+
if (!lock.properties.length || (blocks && lock.properties.length === 1)) {
|
|
376
|
+
ctx.edits.push(replaced(lock, 'true'));
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
renameOption(optionsOf(lock), 'wait', '$wait', lock, ctx);
|
|
284
380
|
}
|
|
285
381
|
}
|
|
286
382
|
function isTextSearch(node) {
|
|
@@ -298,7 +394,7 @@ function rewriteAggregate(agg, ctx) {
|
|
|
298
394
|
for (const op of objectProperties(fn.initializer)) {
|
|
299
395
|
const isStar = ts.isStringLiteralLike(op.initializer) && op.initializer.text === '*';
|
|
300
396
|
if (!isStar) {
|
|
301
|
-
rewriteValue(op.initializer,
|
|
397
|
+
rewriteValue(op.initializer, fieldKeysEdits, op, `write '${op.name.getText()}' as { field: true }`, ctx);
|
|
302
398
|
}
|
|
303
399
|
}
|
|
304
400
|
}
|
|
@@ -307,6 +403,10 @@ function rewriteAggregate(agg, ctx) {
|
|
|
307
403
|
function objectProperties(node) {
|
|
308
404
|
return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
|
|
309
405
|
}
|
|
406
|
+
/** The elements of an array literal, or none where `node` is not one. */
|
|
407
|
+
function arrayElements(node) {
|
|
408
|
+
return node && ts.isArrayLiteralExpression(node) ? node.elements : [];
|
|
409
|
+
}
|
|
310
410
|
/**
|
|
311
411
|
* Drops `declare` from a decorated field. A `declare` field emits nothing, so the standard spec has
|
|
312
412
|
* nothing to decorate and rejects it ("Decorators are not valid here"). Legacy decorators tolerated it
|
|
@@ -337,7 +437,7 @@ function unwrapRelationAlias(node, ctx) {
|
|
|
337
437
|
ts.isIdentifier(declared.typeName) &&
|
|
338
438
|
declared.typeName.text === 'Relation' &&
|
|
339
439
|
declared.typeArguments?.length === 1) {
|
|
340
|
-
ctx.edits.push(
|
|
440
|
+
ctx.edits.push(replaced(declared, declared.typeArguments[0].getText()));
|
|
341
441
|
ctx.relationAlias.unwrapped += 1;
|
|
342
442
|
}
|
|
343
443
|
}
|
|
@@ -368,11 +468,10 @@ function deadImportNames(source, ctx) {
|
|
|
368
468
|
return new Set(seen > 0 && seen === unwrapped ? ['Relation'] : []);
|
|
369
469
|
}
|
|
370
470
|
/**
|
|
371
|
-
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and
|
|
372
|
-
*
|
|
471
|
+
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
|
|
472
|
+
* from the imports {@link renameExports} left as written, having left the dead names out of the rest.
|
|
373
473
|
*/
|
|
374
|
-
function dropDeadImports(source, ctx) {
|
|
375
|
-
const dead = deadImportNames(source, ctx);
|
|
474
|
+
function dropDeadImports(source, dead, rewritten, ctx) {
|
|
376
475
|
for (const statement of source.statements) {
|
|
377
476
|
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
378
477
|
continue;
|
|
@@ -382,7 +481,7 @@ function dropDeadImports(source, ctx) {
|
|
|
382
481
|
continue;
|
|
383
482
|
}
|
|
384
483
|
const named = statement.importClause?.namedBindings;
|
|
385
|
-
if (!named || !ts.isNamedImports(named)) {
|
|
484
|
+
if (!named || !ts.isNamedImports(named) || rewritten.has(statement)) {
|
|
386
485
|
continue;
|
|
387
486
|
}
|
|
388
487
|
const dropped = named.elements.filter((element) => dead.has(element.name.text));
|
|
@@ -411,62 +510,70 @@ function isIdKeyBrand(member) {
|
|
|
411
510
|
* a subclass that replaces an inherited `id` needs the brand precisely because the inherited name
|
|
412
511
|
* would otherwise be taken for the key.
|
|
413
512
|
*
|
|
414
|
-
*
|
|
513
|
+
* One written makes `idKey` a value the file has to import, which is recorded for {@link addImports}.
|
|
415
514
|
*/
|
|
416
515
|
function brandIdKey(node, ctx) {
|
|
417
516
|
const ids = node.members.filter((m) => ts.isPropertyDeclaration(m) && decoratorsOf(m).some((d) => decoratorName(d) === 'Id'));
|
|
418
517
|
if (!ids.length || node.members.some(isIdKeyBrand)) {
|
|
419
|
-
return
|
|
518
|
+
return;
|
|
420
519
|
}
|
|
421
520
|
const names = ids.map((m) => propertyKey(m.name)).filter((name) => name !== undefined);
|
|
422
521
|
if (names.length !== ids.length) {
|
|
423
522
|
ctx.unresolved.push(`${ctx.describe(node)}: a key written as a computed name; add the 'idKey' brand by hand`);
|
|
424
|
-
return
|
|
523
|
+
return;
|
|
425
524
|
}
|
|
426
525
|
if (names.length === 1 && CONVENTIONAL_ID_NAMES.has(names[0])) {
|
|
427
|
-
return
|
|
526
|
+
return;
|
|
428
527
|
}
|
|
429
528
|
const indent = ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(ids[0].getStart()).character);
|
|
430
529
|
const brand = names.map((name) => `'${name}'`).join(' | ');
|
|
431
530
|
const start = node.members.pos;
|
|
432
531
|
ctx.edits.push({ start, end: start, text: `\n${indent}[idKey]?: ${brand};` });
|
|
433
|
-
|
|
532
|
+
ctx.imports.set('idKey', 'the brand(s)');
|
|
434
533
|
}
|
|
435
534
|
/**
|
|
436
|
-
* Imports
|
|
535
|
+
* Imports what the rewrites wrote (`idKey`, `raw`) into the file's own `uql-orm` import. Reported instead
|
|
437
536
|
* where there is none: the package may be imported under a path this codemod does not recognise.
|
|
438
537
|
*/
|
|
439
|
-
function
|
|
440
|
-
|
|
538
|
+
function addImports(source, rewritten, ctx) {
|
|
539
|
+
const imported = new Set(uqlImports(source).map(importedName));
|
|
540
|
+
const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
|
|
541
|
+
if (!missing.length) {
|
|
441
542
|
return;
|
|
442
543
|
}
|
|
443
544
|
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration))?.elements[0];
|
|
444
545
|
if (!anchor) {
|
|
445
|
-
|
|
546
|
+
for (const [name, what] of missing) {
|
|
547
|
+
ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
|
|
548
|
+
}
|
|
446
549
|
return;
|
|
447
550
|
}
|
|
448
|
-
ctx.edits.push(
|
|
551
|
+
ctx.edits.push(inserted(anchor, missing.map(([name]) => `${name}, `).join('')));
|
|
449
552
|
}
|
|
450
|
-
/** Everything the standard spec needs written onto one decorated property. */
|
|
553
|
+
/** Everything the standard spec needs written onto one decorated property, and the options renamed since. */
|
|
451
554
|
function rewriteProperty(node, ctx) {
|
|
555
|
+
const owner = { param: paramFor(node.parent.name?.text), entity: node.parent };
|
|
452
556
|
const decorators = decoratorsOf(node);
|
|
453
557
|
for (const decorator of decorators) {
|
|
454
558
|
const name = decoratorName(decorator);
|
|
455
559
|
if (!name) {
|
|
456
560
|
continue;
|
|
457
561
|
}
|
|
562
|
+
const options = decoratorOptions(decorator);
|
|
458
563
|
if (FIELD_DECORATORS.has(name)) {
|
|
459
564
|
addFieldType(decorator, node, ctx);
|
|
460
|
-
|
|
565
|
+
// Options it cannot read may still hold `virtual`, so they are reported where they mention it.
|
|
566
|
+
if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
|
|
567
|
+
ctx.unresolved.push(`${ctx.describe(node)}: ${options.reason}`);
|
|
568
|
+
}
|
|
569
|
+
renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
|
|
461
570
|
}
|
|
462
|
-
const options = decoratorOptions(decorator);
|
|
463
571
|
if (RELATION_DECORATORS.has(name)) {
|
|
464
572
|
addRelationEntity(decorator, node, ctx);
|
|
465
573
|
}
|
|
466
574
|
if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
|
|
467
|
-
const owner = ts.isClassLike(node.parent) ? node.parent.name?.text : undefined;
|
|
468
575
|
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
469
|
-
rewriteRelationOptions(options.node,
|
|
576
|
+
rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
|
|
470
577
|
}
|
|
471
578
|
}
|
|
472
579
|
if (decorators.length) {
|
|
@@ -496,10 +603,10 @@ function reportRemovedExports(source, ctx) {
|
|
|
496
603
|
}
|
|
497
604
|
/**
|
|
498
605
|
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, renaming every use of it: a name moving
|
|
499
|
-
* entries gets an import from its new one, and a name the file already imports is dropped
|
|
500
|
-
* rewritten imports, which nothing else may edit.
|
|
606
|
+
* entries gets an import from its new one, and a name the file already imports is dropped, as is a `dead`
|
|
607
|
+
* one. Returns the rewritten imports, which nothing else may edit.
|
|
501
608
|
*/
|
|
502
|
-
function renameExports(source, ctx) {
|
|
609
|
+
function renameExports(source, dead, ctx) {
|
|
503
610
|
const imports = uqlImportDeclarations(source, true);
|
|
504
611
|
const kept = imports.flatMap(({ elements }) => elements).filter((element) => !renamedTo(element));
|
|
505
612
|
const bound = new Set(kept.map((element) => element.name.text));
|
|
@@ -509,7 +616,7 @@ function renameExports(source, ctx) {
|
|
|
509
616
|
continue;
|
|
510
617
|
}
|
|
511
618
|
const byEntry = new Map([[entry, []]]);
|
|
512
|
-
for (const element of elements) {
|
|
619
|
+
for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
|
|
513
620
|
const rename = renamedTo(element);
|
|
514
621
|
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
515
622
|
if (rename && !element.propertyName) {
|
|
@@ -579,28 +686,163 @@ function importedName(element) {
|
|
|
579
686
|
return (element.propertyName ?? element.name).text;
|
|
580
687
|
}
|
|
581
688
|
/**
|
|
582
|
-
* Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()
|
|
583
|
-
* string
|
|
584
|
-
* left alone because a template cannot be built from a value that is not known here.
|
|
689
|
+
* Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()`, the callback
|
|
690
|
+
* form's included. A computed string is left alone: a template cannot be built from a value not known here.
|
|
585
691
|
*/
|
|
586
692
|
function rewriteRawCall(node, ctx, source) {
|
|
587
693
|
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== 'raw') {
|
|
588
694
|
return;
|
|
589
695
|
}
|
|
590
696
|
const [expression, alias] = node.arguments;
|
|
591
|
-
if (!expression ||
|
|
697
|
+
if (!expression || node.arguments.length > 2) {
|
|
592
698
|
return;
|
|
593
699
|
}
|
|
594
700
|
const suffix = alias ? `.as(${alias.getText(source)})` : '';
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
701
|
+
if (ts.isStringLiteral(expression)) {
|
|
702
|
+
ctx.edits.push(replaced(node, `${rawTag(expression.text)}${suffix}`));
|
|
703
|
+
}
|
|
704
|
+
else if (alias) {
|
|
705
|
+
ctx.edits.push({ start: expression.getEnd(), end: node.getEnd(), text: `)${suffix}` });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Rewrites each `col('x')` into the ref it names - `product.x` in a `computed` callback, `refs(Item).x` in a
|
|
710
|
+
* statement - where the entity its SQL renders against, and a member named `x`, can be told from here. Every
|
|
711
|
+
* other use is reported. Returns `col` as a dead import once no use is left.
|
|
712
|
+
*/
|
|
713
|
+
function rewriteColumnRefs(source, ctx) {
|
|
714
|
+
const element = uqlImports(source).find((use) => importedName(use) === 'col');
|
|
715
|
+
if (!element) {
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
718
|
+
const computed = new Map();
|
|
719
|
+
const unread = [];
|
|
720
|
+
for (const use of usesOf(source, element.name, ctx.checker)) {
|
|
721
|
+
const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
|
|
722
|
+
const sql = call && ts.findAncestor(call, isRaw);
|
|
723
|
+
const option = sql?.parent;
|
|
724
|
+
const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
|
|
725
|
+
const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
|
|
726
|
+
if (call && sql && ts.isTaggedTemplateExpression(sql) && owner) {
|
|
727
|
+
computed.set(sql, { owner, calls: [...(computed.get(sql)?.calls ?? []), call] });
|
|
728
|
+
}
|
|
729
|
+
else if (edit) {
|
|
730
|
+
ctx.edits.push(edit);
|
|
731
|
+
ctx.imports.set('refs', 'the refs(...)');
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
unread.push(use);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
for (const [sql, { owner, calls }] of computed) {
|
|
738
|
+
const edits = computedRefEdits(sql, calls, owner, ctx);
|
|
739
|
+
if (edits) {
|
|
740
|
+
ctx.edits.push(...edits);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
unread.push(...calls);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const node of unread.sort((a, b) => a.getStart() - b.getStart())) {
|
|
747
|
+
ctx.unresolved.push(`${ctx.describe(node)}: 'col' was removed; read the column off refs(Entity), which the codemod could not tell here`);
|
|
748
|
+
}
|
|
749
|
+
return unread.length ? [] : ['col'];
|
|
750
|
+
}
|
|
751
|
+
function isComputedOption(option) {
|
|
752
|
+
const key = propertyKey(option.name);
|
|
753
|
+
return key === 'computed' || key === 'virtual';
|
|
600
754
|
}
|
|
601
|
-
/**
|
|
602
|
-
function
|
|
603
|
-
|
|
755
|
+
/** The entity a field's `computed` SQL is declared on: the decorated property's class, or `defineEntity`'s first argument. */
|
|
756
|
+
function computedOwner(option) {
|
|
757
|
+
const holder = ts.findAncestor(option, (node) => ts.isPropertyDeclaration(node) ||
|
|
758
|
+
(ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineEntity'));
|
|
759
|
+
if (holder && ts.isPropertyDeclaration(holder)) {
|
|
760
|
+
return { param: paramFor(holder.parent.name?.text), entity: holder.parent };
|
|
761
|
+
}
|
|
762
|
+
const [entity] = holder?.arguments ?? [];
|
|
763
|
+
return entity && { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
|
|
764
|
+
}
|
|
765
|
+
/** `(product) => ` before a `computed` raw, and `product.x` for each `col('x')` in it, where every one names a member. */
|
|
766
|
+
function computedRefEdits(sql, calls, owner, ctx) {
|
|
767
|
+
const members = memberNames(owner.entity, ctx.checker);
|
|
768
|
+
const names = calls.map((call) => columnMember(call, members));
|
|
769
|
+
if (mentions(sql, owner.param) || !names.every((name) => name !== undefined)) {
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
return [
|
|
773
|
+
inserted(sql, `(${owner.param}) => `),
|
|
774
|
+
...calls.map((call, at) => replaced(call, memberAccess(owner.param, names[at]))),
|
|
775
|
+
];
|
|
776
|
+
}
|
|
777
|
+
/** `refs(Item).x` for a `col('x')` in a statement, where the entity its SQL renders against can be told. */
|
|
778
|
+
function statementRefEdit(call, ctx) {
|
|
779
|
+
const entity = statementEntity(call, ctx.checker);
|
|
780
|
+
const name = entity && columnMember(call, propertyNames(entity.type));
|
|
781
|
+
return entity && name !== undefined ? replaced(call, memberAccess(`refs(${entity.name})`, name)) : undefined;
|
|
782
|
+
}
|
|
783
|
+
/** The member a `col('x')` names, where its one argument is a literal naming one of `members`. */
|
|
784
|
+
function columnMember(call, members) {
|
|
785
|
+
const [name] = call.arguments;
|
|
786
|
+
return call.arguments.length === 1 && ts.isStringLiteralLike(name) && members.has(name.text) ? name.text : undefined;
|
|
787
|
+
}
|
|
788
|
+
/** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow. */
|
|
789
|
+
function mentions(node, name) {
|
|
790
|
+
if (ts.isIdentifier(node)) {
|
|
791
|
+
return node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node);
|
|
792
|
+
}
|
|
793
|
+
return ts.forEachChild(node, (child) => mentions(child, name) || undefined) ?? false;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* The entity a statement's SQL renders against at `node`: the one its query names, or a relation a `$populate`
|
|
797
|
+
* reads there. Nothing where a function or a relation filter in between opens a scope of its own.
|
|
798
|
+
*/
|
|
799
|
+
function statementEntity(node, checker) {
|
|
800
|
+
const path = [];
|
|
801
|
+
for (let at = node; at.parent; at = at.parent) {
|
|
802
|
+
const { parent } = at;
|
|
803
|
+
if (ts.isFunctionLike(parent)) {
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
if (ts.isPropertyAssignment(parent)) {
|
|
807
|
+
path.unshift(propertyKey(parent.name) ?? '');
|
|
808
|
+
}
|
|
809
|
+
const queried = ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === at);
|
|
810
|
+
const entity = queried ? queriedEntity(parent, checker) : undefined;
|
|
811
|
+
if (entity) {
|
|
812
|
+
return entityAlong(entity, path, node, checker);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
/** The entity a call queries: its first argument's class, or the `$entity` its query names. */
|
|
818
|
+
function queriedEntity(call, checker) {
|
|
819
|
+
const [first] = call.arguments;
|
|
820
|
+
const entity = first && ts.isObjectLiteralExpression(first) ? propertyValue(first, '$entity') : first;
|
|
821
|
+
const instance = entity && checker.getTypeAtLocation(entity).getConstructSignatures()[0]?.getReturnType();
|
|
822
|
+
return entity && ts.isIdentifier(entity) && instance ? { name: entity.text, type: instance } : undefined;
|
|
823
|
+
}
|
|
824
|
+
/** `entity` followed along `path`: into each relation a `$populate` names, and nowhere a relation filter opens. */
|
|
825
|
+
function entityAlong(entity, path, node, checker) {
|
|
826
|
+
let current = entity;
|
|
827
|
+
for (const [at, key] of path.entries()) {
|
|
828
|
+
const property = current.type.getProperty(key);
|
|
829
|
+
const related = property && relatedClass(property, node, checker);
|
|
830
|
+
if (!related) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const inScope = checker.getSymbolsInScope(node, ts.SymbolFlags.Class).some(({ name }) => name === related.name);
|
|
834
|
+
if (path[at - 1] !== '$populate' || !inScope) {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
current = { name: related.name, type: checker.getDeclaredTypeOfSymbol(related) };
|
|
838
|
+
}
|
|
839
|
+
return current;
|
|
840
|
+
}
|
|
841
|
+
/** The class a relation property points at, one or many, or nothing where the property holds no class. */
|
|
842
|
+
function relatedClass(property, node, checker) {
|
|
843
|
+
const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(property, node));
|
|
844
|
+
const symbol = (checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type).getSymbol();
|
|
845
|
+
return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
|
|
604
846
|
}
|
|
605
847
|
/** Rewrites one source file for the standard decorator spec. */
|
|
606
848
|
export function transformFile(source, checker) {
|
|
@@ -609,12 +851,14 @@ export function transformFile(source, checker) {
|
|
|
609
851
|
edits: [],
|
|
610
852
|
unresolved: [],
|
|
611
853
|
notes: [],
|
|
854
|
+
imports: new Map(),
|
|
612
855
|
relationAlias: { seen: 0, unwrapped: 0 },
|
|
613
856
|
describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
|
|
614
857
|
};
|
|
615
858
|
const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
|
|
616
859
|
const rewritesRaw = uqlImports(source).some((element) => importedName(element) === 'raw');
|
|
617
|
-
|
|
860
|
+
// The builder's methods are known by name alone, so they are read only in a file that imports uql-orm.
|
|
861
|
+
const importsUql = uqlImportDeclarations(source, true).length > 0;
|
|
618
862
|
const visit = (node) => {
|
|
619
863
|
reportRemovedDecorators(node, ctx);
|
|
620
864
|
countRelationAlias(node, ctx);
|
|
@@ -625,24 +869,26 @@ export function transformFile(source, checker) {
|
|
|
625
869
|
rewriteProperty(node, ctx);
|
|
626
870
|
}
|
|
627
871
|
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
|
628
|
-
|
|
872
|
+
brandIdKey(node, ctx);
|
|
629
873
|
rewriteClassDecorators(node, ctx);
|
|
630
874
|
}
|
|
631
875
|
if (ts.isCallExpression(node)) {
|
|
632
876
|
rewriteDefineCall(node, ctx);
|
|
633
877
|
}
|
|
878
|
+
if (importsUql && ts.isCallExpression(node)) {
|
|
879
|
+
rewriteBuilderCall(node, ctx);
|
|
880
|
+
}
|
|
634
881
|
if (ts.isPropertyAssignment(node)) {
|
|
635
882
|
rewriteStatementKeys(node, ctx);
|
|
636
883
|
}
|
|
637
884
|
ts.forEachChild(node, visit);
|
|
638
885
|
};
|
|
639
886
|
visit(source);
|
|
640
|
-
const
|
|
887
|
+
const dead = new Set([...deadImportNames(source, ctx), ...rewriteColumnRefs(source, ctx)]);
|
|
888
|
+
const rewritten = renameExports(source, dead, ctx);
|
|
641
889
|
reportRemovedExports(source, ctx);
|
|
642
|
-
dropDeadImports(source, ctx);
|
|
643
|
-
|
|
644
|
-
addIdKeyImport(source, rewritten, ctx);
|
|
645
|
-
}
|
|
890
|
+
dropDeadImports(source, dead, rewritten, ctx);
|
|
891
|
+
addImports(source, rewritten, ctx);
|
|
646
892
|
return {
|
|
647
893
|
fileName: source.fileName,
|
|
648
894
|
text: applyEdits(source.getFullText(), ctx.edits),
|