ts-const-value-transformer 0.2.1 → 0.4.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/CHANGELOG.md +8 -0
- package/README.md +23 -0
- package/dist/transform.d.mts +8 -0
- package/dist/transform.mjs +108 -7
- package/dist/version.d.mts +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.0
|
|
4
|
+
|
|
5
|
+
Add `unsafeHoistWritableValues` option to prevent from hoisting non-const values with literal type unexpectedly
|
|
6
|
+
|
|
7
|
+
## v0.3.0
|
|
8
|
+
|
|
9
|
+
Add `externalNames` option for `hoistExternalValues`
|
|
10
|
+
|
|
3
11
|
## v0.2.1
|
|
4
12
|
|
|
5
13
|
Fix for tracing imported symbol
|
package/README.md
CHANGED
|
@@ -160,6 +160,14 @@ export interface TransformOptions {
|
|
|
160
160
|
hoistPureFunctionCall?: boolean | undefined;
|
|
161
161
|
/** Hoist expressions with `as XXX`. Default is false because the base (non-`as`) value may be non-constant. */
|
|
162
162
|
unsafeHoistAsExpresion?: boolean | undefined;
|
|
163
|
+
/** Hoist properties/variables that can write (i.e. `let` / `var` variables or properies without `readonly`). Default is false because although the value is literal type at some point, the value may change to another literal type. */
|
|
164
|
+
unsafeHoistWritableValues?: boolean | undefined;
|
|
165
|
+
/**
|
|
166
|
+
* External names (tested with `.includes()` for string, with `.test()` for RegExp) for `hoistExternalValues` settings (If `hoistExternalValues` is not specified, this setting will be used).
|
|
167
|
+
* - Path separators for input file name are always normalized to '/' internally.
|
|
168
|
+
* - Default is `['/node_modules/']`.
|
|
169
|
+
*/
|
|
170
|
+
externalNames?: ReadonlyArray<string | RegExp> | undefined;
|
|
163
171
|
}
|
|
164
172
|
```
|
|
165
173
|
|
|
@@ -247,6 +255,21 @@ The version string of this package.
|
|
|
247
255
|
|
|
248
256
|
See [Transform options](#transform-options).
|
|
249
257
|
|
|
258
|
+
## Notice
|
|
259
|
+
|
|
260
|
+
Starting from v0.4.0, `unsafeHoistWritableValues` option is introduced. Since TypeScript sometimes narrows non-constant values to literal types such as:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const resultObject = { success: false };
|
|
264
|
+
someFunc1(resultObject);
|
|
265
|
+
console.log(resultObject.success); // resultObject.success will be `boolean` type
|
|
266
|
+
resultObject.success = false;
|
|
267
|
+
someFunc1(resultObject);
|
|
268
|
+
console.log(resultObject.success); // resultObject.success will be `false` type, not `boolean`
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
... so if `unsafeHoistWritableValues` is true, the second reference of `resultObject.success` above will be replaced to `false`, which may not be correct.
|
|
272
|
+
|
|
250
273
|
## Additional notes
|
|
251
274
|
|
|
252
275
|
I think there should be more optimization methods by using strictly-typed information, like other programming languages.
|
package/dist/transform.d.mts
CHANGED
|
@@ -15,6 +15,14 @@ export interface TransformOptions {
|
|
|
15
15
|
hoistPureFunctionCall?: boolean | undefined;
|
|
16
16
|
/** Hoist expressions with `as XXX`. Default is false because the base (non-`as`) value may be non-constant. */
|
|
17
17
|
unsafeHoistAsExpresion?: boolean | undefined;
|
|
18
|
+
/** Hoist properties/variables that can write (i.e. `let` / `var` variables or properies without `readonly`). Default is false because although the value is literal type at some point, the value may change to another literal type. */
|
|
19
|
+
unsafeHoistWritableValues?: boolean | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* External names (tested with `.includes()` for string, with `.test()` for RegExp) for `hoistExternalValues` settings (If `hoistExternalValues` is not specified, this setting will be used).
|
|
22
|
+
* - Path separators for input file name are always normalized to '/' internally.
|
|
23
|
+
* - Default is `['/node_modules/']`.
|
|
24
|
+
*/
|
|
25
|
+
externalNames?: ReadonlyArray<string | RegExp> | undefined;
|
|
18
26
|
}
|
|
19
27
|
export declare function transformSource(sourceFile: ts.SourceFile, program: ts.Program, context: ts.TransformationContext, options?: TransformOptions): ts.SourceFile;
|
|
20
28
|
export declare function printSource(sourceFile: ts.SourceFile): string;
|
package/dist/transform.mjs
CHANGED
|
@@ -11,6 +11,8 @@ function assignDefaultValues(options = {}) {
|
|
|
11
11
|
unsafeHoistAsExpresion: options.unsafeHoistAsExpresion ?? false,
|
|
12
12
|
hoistPureFunctionCall: options.hoistPureFunctionCall ?? false,
|
|
13
13
|
unsafeHoistFunctionCall: options.unsafeHoistFunctionCall ?? false,
|
|
14
|
+
unsafeHoistWritableValues: options.unsafeHoistWritableValues ?? false,
|
|
15
|
+
externalNames: options.externalNames ?? [],
|
|
14
16
|
};
|
|
15
17
|
}
|
|
16
18
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -55,7 +57,8 @@ function visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options
|
|
|
55
57
|
return node;
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
|
-
if (!options.hoistExternalValues &&
|
|
60
|
+
if (!options.hoistExternalValues &&
|
|
61
|
+
isExternalReference(node, program, options.externalNames)) {
|
|
59
62
|
return node;
|
|
60
63
|
}
|
|
61
64
|
if (ts.isIdentifier(node)) {
|
|
@@ -65,9 +68,7 @@ function visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options
|
|
|
65
68
|
(!ts.isExpression(node.parent) &&
|
|
66
69
|
(!('initializer' in node.parent) ||
|
|
67
70
|
node !== node.parent.initializer)) ||
|
|
68
|
-
(
|
|
69
|
-
ts.isPropertyAccessExpression(node.parent) &&
|
|
70
|
-
node === node.parent.name)) {
|
|
71
|
+
(ts.isPropertyAccessExpression(node.parent) && node === node.parent.name)) {
|
|
71
72
|
return node;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -76,6 +77,12 @@ function visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options
|
|
|
76
77
|
hasParentAsExpression(node.parent, context, ts))) {
|
|
77
78
|
return node;
|
|
78
79
|
}
|
|
80
|
+
if (!options.unsafeHoistWritableValues) {
|
|
81
|
+
const r = isReadonlyExpression(node, program, ts);
|
|
82
|
+
if (r === false) {
|
|
83
|
+
return node;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
79
86
|
try {
|
|
80
87
|
const typeChecker = program.getTypeChecker();
|
|
81
88
|
const type = typeChecker.getTypeAtLocation(node);
|
|
@@ -132,13 +139,28 @@ function isEnumIdentifier(node, program, tsInstance) {
|
|
|
132
139
|
const type = typeChecker.getTypeAtLocation(node);
|
|
133
140
|
return (type.getFlags() & ts.TypeFlags.EnumLiteral) !== 0;
|
|
134
141
|
}
|
|
135
|
-
function isExternalReference(node, program) {
|
|
142
|
+
function isExternalReference(node, program, externalNames) {
|
|
136
143
|
const typeChecker = program.getTypeChecker();
|
|
137
144
|
const nodeSym = typeChecker.getSymbolAtLocation(node);
|
|
138
145
|
let nodeFrom = nodeSym?.getDeclarations()?.[0];
|
|
139
146
|
while (nodeFrom) {
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
const sourceFileName = nodeFrom.getSourceFile();
|
|
148
|
+
if (externalNames.length === 0) {
|
|
149
|
+
if (/[\\/]node_modules[\\/]/.test(sourceFileName.fileName)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
if (externalNames.some((part) => {
|
|
155
|
+
if (typeof part === 'string') {
|
|
156
|
+
return sourceFileName.fileName.replace(/\\/g, '/').includes(part);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
return part.test(sourceFileName.fileName);
|
|
160
|
+
}
|
|
161
|
+
})) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
142
164
|
}
|
|
143
165
|
// Walk into the 'import' variables
|
|
144
166
|
if (!ts.isImportSpecifier(nodeFrom)) {
|
|
@@ -216,6 +238,85 @@ function hasPureAnnotation(node, sourceFile, tsInstance) {
|
|
|
216
238
|
}
|
|
217
239
|
return false;
|
|
218
240
|
}
|
|
241
|
+
function getMemberName(m, tsInstance) {
|
|
242
|
+
if (!m || !m.name) {
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
const name = m.name;
|
|
246
|
+
if (tsInstance.isIdentifier(name)) {
|
|
247
|
+
return name.escapedText;
|
|
248
|
+
}
|
|
249
|
+
else if (tsInstance.isPrivateIdentifier(name)) {
|
|
250
|
+
return name.escapedText;
|
|
251
|
+
}
|
|
252
|
+
else if (tsInstance.isStringLiteral(name)) {
|
|
253
|
+
return name.text;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
return '';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function isReadonlyPropertyAccess(a, typeChecker, tsInstance) {
|
|
260
|
+
const ts = tsInstance;
|
|
261
|
+
const type = typeChecker.getTypeAtLocation(a.expression);
|
|
262
|
+
const memberName = a.name.getText();
|
|
263
|
+
if (type.getFlags() & ts.TypeFlags.Object) {
|
|
264
|
+
const dummyTypeNode = typeChecker.typeToTypeNode(type, a, ts.NodeBuilderFlags.NoTruncation);
|
|
265
|
+
if (dummyTypeNode && ts.isTypeLiteralNode(dummyTypeNode)) {
|
|
266
|
+
for (let i = 0; i < dummyTypeNode.members.length; ++i) {
|
|
267
|
+
const m = dummyTypeNode.members[i];
|
|
268
|
+
if (m &&
|
|
269
|
+
getMemberName(m, ts) === memberName &&
|
|
270
|
+
ts.isPropertySignature(m)) {
|
|
271
|
+
if (m.modifiers?.some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword)) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const prop = type.getProperty(memberName);
|
|
278
|
+
if (prop && prop.declarations && prop.declarations.length > 0) {
|
|
279
|
+
const decl = prop.declarations[0];
|
|
280
|
+
if (ts.isPropertySignature(decl) &&
|
|
281
|
+
decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword)) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (ts.isVariableDeclaration(decl) &&
|
|
285
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
286
|
+
decl.parent &&
|
|
287
|
+
ts.isVariableDeclarationList(decl.parent) &&
|
|
288
|
+
decl.parent.flags & ts.NodeFlags.Const) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
function isReadonlyExpression(node, program, tsInstance) {
|
|
296
|
+
const ts = tsInstance;
|
|
297
|
+
const typeChecker = program.getTypeChecker();
|
|
298
|
+
if (ts.isIdentifier(node) &&
|
|
299
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
300
|
+
node.parent &&
|
|
301
|
+
!ts.isPropertyAccessExpression(node.parent)) {
|
|
302
|
+
const nodeSym = typeChecker.getSymbolAtLocation(node);
|
|
303
|
+
if (nodeSym?.valueDeclaration) {
|
|
304
|
+
if (ts.isVariableDeclarationList(nodeSym.valueDeclaration.parent)) {
|
|
305
|
+
if (nodeSym.valueDeclaration.parent.flags & ts.NodeFlags.Const) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
313
|
+
if (isEnumAccess(node, program, ts)) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
return isReadonlyPropertyAccess(node, typeChecker, ts);
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
219
320
|
////////////////////////////////////////////////////////////////////////////////
|
|
220
321
|
export function printSource(sourceFile) {
|
|
221
322
|
return printSourceImpl(sourceFile)[0];
|
package/dist/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.
|
|
1
|
+
declare const _default: "0.4.0";
|
|
2
2
|
export default _default;
|
package/dist/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '0.
|
|
1
|
+
export default '0.4.0';
|