ts-const-value-transformer 0.4.0 → 0.5.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.0
4
+
5
+ - Add `useUndefinedSymbolForUndefinedValue` and `hoistUndefinedSymbol` options
6
+ - Add `createPortalTransformer` to use transformer easily
7
+ - Skip update process if unchanged
8
+ - Skip some declarations for performance
9
+ - Add hoisting computed property name
10
+ - Add test for element access
11
+ - Avoid hosting indexed property access
12
+ - Add test case for OmittedExpression and SatisfiesExpression
13
+ - Refactor to avoid handling unexpected expressions
14
+
15
+ ## v0.4.1
16
+
17
+ Fix for not hositing some more unary expressions and parenthesized expression
18
+
3
19
  ## v0.4.0
4
20
 
5
21
  Add `unsafeHoistWritableValues` option to prevent from hoisting non-const values with literal type unexpectedly
package/README.md CHANGED
@@ -162,12 +162,25 @@ export interface TransformOptions {
162
162
  unsafeHoistAsExpresion?: boolean | undefined;
163
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
164
  unsafeHoistWritableValues?: boolean | undefined;
165
+ /** Uses `undefined` symbol for `undefined` type values. Default is false and replaces to `void 0`. */
166
+ useUndefinedSymbolForUndefinedValue?: boolean | undefined;
167
+ /** Hoist `undefined` symbol to `void 0` (or `undefined` if useUndefinedSymbolForUndefinedValue is true). Default is true. */
168
+ hoistUndefinedSymbol?: boolean | undefined;
165
169
  /**
166
170
  * 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
171
  * - Path separators for input file name are always normalized to '/' internally.
168
172
  * - Default is `['/node_modules/']`.
169
173
  */
170
174
  externalNames?: ReadonlyArray<string | RegExp> | undefined;
175
+ /**
176
+ * Specifies for file name list or function to skip transformation. This option is used by webpack loader, the transformed called from ts-loader, and createPortalTransformer only.
177
+ * - For list, if the token is `string`, the transformation will be skipped if `fileName.indexOf(token) >= 0` is true.
178
+ * If the token is `RegExp`, the transformation will be skipped if `fileName.indexOf(token) >= 0` is true.
179
+ * - For function, the transformation will be skipped if `fn(fileName)` is true.
180
+ */
181
+ ignoreFiles?:
182
+ | ReadonlyArray<string | RegExp>
183
+ | ((fileName: string) => boolean);
171
184
  }
172
185
  ```
173
186
 
@@ -183,6 +196,10 @@ import {
183
196
  transformSource,
184
197
  version,
185
198
  type TransformOptions,
199
+ createPortalTransformer,
200
+ createPortalTransformerSync,
201
+ type CreatePortalTransformerOptions,
202
+ type PortalTransformer,
186
203
  } from 'ts-const-value-transformer';
187
204
  ```
188
205
 
@@ -245,7 +262,7 @@ Prints (generates) source code from `SourceFile`, along with raw source-map data
245
262
 
246
263
  #### transformSource: (sourceFile: ts.SourceFile, program: ts.Program, context: ts.TransformationContext, options?: TransformOptions) => ts.SourceFile
247
264
 
248
- Transforms the source file with TypeScript project. You don't need to call this function directly; use `createTransformer` instead.
265
+ Transforms the source file with TypeScript project. You don't need to call this function directly; use `createTransformer` or `createPortalTransformer` instead.
249
266
 
250
267
  #### version: string
251
268
 
@@ -255,6 +272,15 @@ The version string of this package.
255
272
 
256
273
  See [Transform options](#transform-options).
257
274
 
275
+ #### createPortalTransformer: (options?: CreatePortalTransformerOptions) => Promise<PortalTransformer>
276
+
277
+ Creates 'portal transformer', which can be used the transformer easily from the code which does not use TypeScript Compiler API.
278
+ The return object has `transform` method with signature: `(content: string, fileName: string, sourceMap?: string | RawSourceMap | null, options?: TransformOptions) => [newSource: string, newSourceMap: RawSourceMap | undefined]`. You can call to transform TypeScript source code. (Note that this API does not transpile to JavaScript; the output code is still TypeScript code.)
279
+
280
+ `CreatePortalTransformerOptions` has three optional options: `project` (path to tsconfig.json), `typescript` (package path to `typescript` or `typescript` namespace object), and `cwd` (current directory for file search). Also, `ignoreFiles` can be used.
281
+
282
+ If `Promise` cannot be used for some reason, use `createPortalTransformerSync` instead.
283
+
258
284
  ## Notice
259
285
 
260
286
  Starting from v0.4.0, `unsafeHoistWritableValues` option is introduced. Since TypeScript sometimes narrows non-constant values to literal types such as:
@@ -0,0 +1,42 @@
1
+ import type { RawSourceMap } from 'source-map';
2
+ import type * as tsNamespace from 'typescript';
3
+ import { type TransformOptions } from './transform.mjs';
4
+ export interface CreatePortalTransformerOptions extends TransformOptions {
5
+ project?: string;
6
+ typescript?: string | typeof tsNamespace;
7
+ cwd?: string;
8
+ }
9
+ export interface PortalTransformer {
10
+ /** The `typescript` namespace object */
11
+ readonly ts: typeof tsNamespace;
12
+ /** Active `Program` instance for the transformer */
13
+ readonly program: tsNamespace.Program;
14
+ /**
15
+ * Performs transformation.
16
+ * @param content Base source code. If null, uses loaded source code in the TS project.
17
+ * @param fileName Base file name (If not included in the TS project, transformation will not be performed.)
18
+ * @param sourceMap Base source map if exists
19
+ * @param options Transform options (addition to `options` passed to `createPortalTransformer`)
20
+ * @returns Tuple of new source code and source map. Source map may be undefined if source code is unchanged.
21
+ */
22
+ transform(content: string, fileName: string, sourceMap?: string | RawSourceMap | null, options?: TransformOptions): [newSource: string, newSourceMap: RawSourceMap | undefined];
23
+ /**
24
+ * Performs transformation.
25
+ * @param content Base source code. If null, uses loaded source code in the TS project.
26
+ * @param fileName Base file name (If not included in the TS project, transformation will not be performed.)
27
+ * @param sourceMap Base source map if exists
28
+ * @param options Transform options (addition to `options` passed to `createPortalTransformer`)
29
+ * @returns Tuple of new source code and source map. Source map may be undefined if source code is unchanged.
30
+ */
31
+ transform(content: string | null, fileName: string, sourceMap?: string | RawSourceMap | null, options?: TransformOptions): [newSource: string | null, newSourceMap: RawSourceMap | undefined];
32
+ }
33
+ /**
34
+ * Creates the new portal transformer instance for the TS project.
35
+ * After creation, the transformation process can be performed by calling {@link PortalTransformer.transform}.
36
+ */
37
+ export default function createPortalTransformer(options?: CreatePortalTransformerOptions): Promise<PortalTransformer>;
38
+ /**
39
+ * Creates the new portal transformer instance for the TS project (using `require` function).
40
+ * After creation, the transformation process can be performed by calling {@link PortalTransformer.transform}.
41
+ */
42
+ export declare function createPortalTransformerSync(options?: CreatePortalTransformerOptions): PortalTransformer;
@@ -0,0 +1,126 @@
1
+ import * as fs from 'fs';
2
+ import { createRequire } from 'module';
3
+ import * as path from 'path';
4
+ import createTransformer from './createTransformer.mjs';
5
+ import { printSourceWithMap } from './transform.mjs';
6
+ const require = createRequire(import.meta.url);
7
+ function createPortalTransformerImpl(options, ts) {
8
+ const project = options.project ?? 'tsconfig.json';
9
+ let ignoreFiles = options.ignoreFiles ?? [];
10
+ if (typeof ignoreFiles !== 'function') {
11
+ const a = ignoreFiles;
12
+ ignoreFiles = (fileName) => {
13
+ return a.some((t) => {
14
+ if (typeof t === 'string') {
15
+ return fileName.indexOf(t) >= 0;
16
+ }
17
+ else {
18
+ return t.test(fileName);
19
+ }
20
+ });
21
+ };
22
+ }
23
+ const cwd = options.cwd ?? process.cwd();
24
+ const getCurrentDirectory = () => cwd;
25
+ const config = ts.getParsedCommandLineOfConfigFile(project, void 0, {
26
+ fileExists: fs.existsSync,
27
+ getCurrentDirectory,
28
+ // eslint-disable-next-line @typescript-eslint/unbound-method
29
+ readDirectory: ts.sys.readDirectory,
30
+ readFile: (file) => fs.readFileSync(path.isAbsolute(file) ? file : path.join(getCurrentDirectory(), file), 'utf-8'),
31
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
32
+ onUnRecoverableConfigFileDiagnostic: (diag) => {
33
+ throw new Error(ts.formatDiagnostics([diag], {
34
+ getCanonicalFileName: (f) => f,
35
+ getCurrentDirectory,
36
+ getNewLine: () => '\n',
37
+ }));
38
+ },
39
+ });
40
+ if (!config) {
41
+ throw new Error(`[ts-const-value-transformer] Unable to load tsconfig file (effective name = '${project}')`);
42
+ }
43
+ const program = ts.createProgram({
44
+ options: config.options,
45
+ rootNames: config.fileNames,
46
+ });
47
+ return {
48
+ ts,
49
+ program,
50
+ transform: (content, fileName, sourceMap, individualOptions) => {
51
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
52
+ const rawSourceMap = typeof sourceMap === 'string'
53
+ ? JSON.parse(sourceMap)
54
+ : (sourceMap ?? void 0);
55
+ if (ignoreFiles(fileName)) {
56
+ return [content, rawSourceMap];
57
+ }
58
+ const sourceFile = program.getSourceFile(fileName);
59
+ if (!sourceFile) {
60
+ return [content, rawSourceMap];
61
+ }
62
+ // If input content is changed, replace it
63
+ if (content != null && sourceFile.getFullText() !== content) {
64
+ sourceFile.update(content, {
65
+ span: { start: 0, length: sourceFile.end },
66
+ newLength: content.length,
67
+ });
68
+ }
69
+ const transformer = createTransformer(program, {
70
+ options: { ...options, ...individualOptions, ts },
71
+ });
72
+ const result = ts.transform(sourceFile, [transformer], program.getCompilerOptions());
73
+ const transformedSource = result.transformed[0];
74
+ // If unchanged, return base file as-is
75
+ if (transformedSource === sourceFile) {
76
+ return [content ?? sourceFile.getFullText(), rawSourceMap];
77
+ }
78
+ const printed = printSourceWithMap(transformedSource, fileName, rawSourceMap);
79
+ return printed;
80
+ },
81
+ };
82
+ }
83
+ /**
84
+ * Creates the new portal transformer instance for the TS project.
85
+ * After creation, the transformation process can be performed by calling {@link PortalTransformer.transform}.
86
+ */
87
+ export default async function createPortalTransformer(options = {}) {
88
+ let ts;
89
+ if (options.typescript != null) {
90
+ if (typeof options.typescript === 'string') {
91
+ ts = (await import(options.typescript));
92
+ }
93
+ else {
94
+ ts = options.typescript;
95
+ }
96
+ }
97
+ else if (options.ts != null) {
98
+ ts = options.ts;
99
+ }
100
+ else {
101
+ ts = await import('typescript');
102
+ }
103
+ return createPortalTransformerImpl(options, ts);
104
+ }
105
+ /**
106
+ * Creates the new portal transformer instance for the TS project (using `require` function).
107
+ * After creation, the transformation process can be performed by calling {@link PortalTransformer.transform}.
108
+ */
109
+ export function createPortalTransformerSync(options = {}) {
110
+ let ts;
111
+ if (options.typescript != null) {
112
+ if (typeof options.typescript === 'string') {
113
+ ts = require(options.typescript);
114
+ }
115
+ else {
116
+ ts = options.typescript;
117
+ }
118
+ }
119
+ else if (options.ts != null) {
120
+ ts = options.ts;
121
+ }
122
+ else {
123
+ ts = require('typescript');
124
+ }
125
+ return createPortalTransformerImpl(options, ts);
126
+ }
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
+ import createPortalTransformer, { createPortalTransformerSync, type CreatePortalTransformerOptions, type PortalTransformer } from './createPortalTransformer.mjs';
1
2
  import createTransformer from './createTransformer.mjs';
2
3
  import version from './version.mjs';
3
4
  export { printSource, printSourceWithMap, transformSource, type TransformOptions, } from './transform.mjs';
4
- export { createTransformer, version };
5
+ export { createPortalTransformer, createPortalTransformerSync, createTransformer, type CreatePortalTransformerOptions, type PortalTransformer, version, };
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
+ import createPortalTransformer, { createPortalTransformerSync, } from './createPortalTransformer.mjs';
1
2
  import createTransformer from './createTransformer.mjs';
2
3
  import version from './version.mjs';
3
4
  export { printSource, printSourceWithMap, transformSource, } from './transform.mjs';
4
- export { createTransformer, version };
5
+ export { createPortalTransformer, createPortalTransformerSync, createTransformer, version, };
package/dist/loader.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type * as tsNamespace from 'typescript';
2
2
  import type * as webpack from 'webpack';
3
- import { type TransformOptions } from './transform.mjs';
3
+ import type { TransformOptions } from './transform.mjs';
4
4
  export interface TsConstValueTransformerLoaderOptions extends TransformOptions {
5
5
  project?: string;
6
6
  typescript?: string | typeof tsNamespace;
package/dist/loader.mjs CHANGED
@@ -1,74 +1,22 @@
1
- import * as fs from 'fs';
2
1
  import * as path from 'path';
3
- import createTransformer from './createTransformer.mjs';
4
- import { printSourceWithMap } from './transform.mjs';
5
- const programMap = new Map();
2
+ import createPortalTransformer, {} from './createPortalTransformer.mjs';
3
+ const transformerMap = new Map();
6
4
  const loader = function (content, sourceMap) {
7
5
  this.async();
8
6
  void (async () => {
9
7
  try {
10
8
  const options = this.getOptions() || {};
11
- let ts;
12
- if (options.typescript != null) {
13
- if (typeof options.typescript === 'string') {
14
- ts = (await import(options.typescript));
15
- }
16
- else {
17
- ts = options.typescript;
18
- }
19
- }
20
- else if (options.ts != null) {
21
- ts = options.ts;
22
- }
23
- else {
24
- ts = await import('typescript');
25
- }
26
9
  const project = options.project ?? 'tsconfig.json';
27
- let program = programMap.get(project);
28
- if (!program) {
29
- const getCurrentDirectory = () => path.dirname(this.resourcePath);
30
- const config = ts.getParsedCommandLineOfConfigFile(project, void 0, {
31
- fileExists: fs.existsSync,
32
- getCurrentDirectory,
33
- // eslint-disable-next-line @typescript-eslint/unbound-method
34
- readDirectory: ts.sys.readDirectory,
35
- readFile: (file) => fs.readFileSync(path.isAbsolute(file)
36
- ? file
37
- : path.join(getCurrentDirectory(), file), 'utf-8'),
38
- useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
39
- onUnRecoverableConfigFileDiagnostic: (diag) => {
40
- throw new Error(ts.formatDiagnostics([diag], {
41
- getCanonicalFileName: (f) => f,
42
- getCurrentDirectory,
43
- getNewLine: () => '\n',
44
- }));
45
- },
10
+ let transformer = transformerMap.get(project);
11
+ if (!transformer) {
12
+ transformer = await createPortalTransformer({
13
+ ...options,
14
+ cwd: path.dirname(this.resourcePath),
46
15
  });
47
- if (!config) {
48
- throw new Error(`[ts-const-value-transformer-loader] Unable to load tsconfig file (effective name = '${project}')`);
49
- }
50
- program = ts.createProgram({
51
- options: config.options,
52
- rootNames: config.fileNames,
53
- });
54
- programMap.set(project, program);
55
- }
56
- const sourceFile = program.getSourceFile(this.resource);
57
- if (!sourceFile) {
58
- throw new Error(`'${this.resource}' is not in the TypeScript project.`);
16
+ transformerMap.set(project, transformer);
59
17
  }
60
- sourceFile.update(content, {
61
- span: { start: 0, length: sourceFile.end },
62
- newLength: content.length,
63
- });
64
- const transformer = createTransformer(program, {
65
- options: { ...options, ts },
66
- });
67
- const result = ts.transform(sourceFile, [transformer], program.getCompilerOptions());
68
- const printed = printSourceWithMap(result.transformed[0], this.resource,
69
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
70
- typeof sourceMap === 'string' ? JSON.parse(sourceMap) : sourceMap);
71
- this.callback(null, printed[0], printed[1]);
18
+ const result = transformer.transform(content, this.resource, sourceMap);
19
+ this.callback(null, result[0], result[1]);
72
20
  }
73
21
  catch (e) {
74
22
  this.callback(e);
@@ -17,12 +17,23 @@ export interface TransformOptions {
17
17
  unsafeHoistAsExpresion?: boolean | undefined;
18
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
19
  unsafeHoistWritableValues?: boolean | undefined;
20
+ /** Uses `undefined` symbol for `undefined` type values. Default is false and replaces to `void 0`. */
21
+ useUndefinedSymbolForUndefinedValue?: boolean | undefined;
22
+ /** Hoist `undefined` symbol to `void 0` (or `undefined` if {@linkcode useUndefinedSymbolForUndefinedValue} is true). Default is true. */
23
+ hoistUndefinedSymbol?: boolean | undefined;
20
24
  /**
21
25
  * 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
26
  * - Path separators for input file name are always normalized to '/' internally.
23
27
  * - Default is `['/node_modules/']`.
24
28
  */
25
29
  externalNames?: ReadonlyArray<string | RegExp> | undefined;
30
+ /**
31
+ * Specifies for file name list or function to skip transformation. This option is used by webpack loader, the transformed called from ts-loader, and {@link createPortalTransformer} only.
32
+ * - For list, if the token is `string`, the transformation will be skipped if `fileName.indexOf(token) >= 0` is true.
33
+ * If the token is `RegExp`, the transformation will be skipped if `fileName.indexOf(token) >= 0` is true.
34
+ * - For function, the transformation will be skipped if `fn(fileName)` is true.
35
+ */
36
+ ignoreFiles?: ReadonlyArray<string | RegExp> | ((fileName: string) => boolean);
26
37
  }
27
38
  export declare function transformSource(sourceFile: ts.SourceFile, program: ts.Program, context: ts.TransformationContext, options?: TransformOptions): ts.SourceFile;
28
39
  export declare function printSource(sourceFile: ts.SourceFile): string;
@@ -12,69 +12,87 @@ function assignDefaultValues(options = {}) {
12
12
  hoistPureFunctionCall: options.hoistPureFunctionCall ?? false,
13
13
  unsafeHoistFunctionCall: options.unsafeHoistFunctionCall ?? false,
14
14
  unsafeHoistWritableValues: options.unsafeHoistWritableValues ?? false,
15
+ useUndefinedSymbolForUndefinedValue: options.useUndefinedSymbolForUndefinedValue ?? false,
16
+ hoistUndefinedSymbol: options.hoistUndefinedSymbol ?? true,
15
17
  externalNames: options.externalNames ?? [],
18
+ ignoreFiles: options.ignoreFiles ?? [],
16
19
  };
17
20
  }
18
21
  ////////////////////////////////////////////////////////////////////////////////
19
22
  export function transformSource(sourceFile, program, context, options) {
20
- return visitNodeChildren(sourceFile, sourceFile, program, context, assignDefaultValues(options));
23
+ return visitNodeChildren(sourceFile, sourceFile, sourceFile, program, context, assignDefaultValues(options));
21
24
  }
22
- function visitNodeChildren(node, sourceFile, program, context, options) {
23
- const newNode = visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options);
25
+ function visitNodeChildren(node, parent, sourceFile, program, context, options) {
26
+ const newNode = visitNodeAndReplaceIfNeeded(node, parent, sourceFile, program, context, options);
24
27
  if (newNode[SYMBOL_ORIGINAL_NODE]) {
25
28
  return newNode;
26
29
  }
27
- return ts.visitEachChild(newNode, (node) => visitNodeChildren(node, sourceFile, program, context, options), context);
30
+ // skip children for satisifes expression
31
+ if (ts.isSatisfiesExpression(newNode)) {
32
+ return newNode;
33
+ }
34
+ // skip statements which would not have 'value' expressions
35
+ if (ts.isInterfaceDeclaration(newNode) ||
36
+ ts.isTypeAliasDeclaration(newNode) ||
37
+ // Identifies in import clause should not be parsed
38
+ ts.isImportDeclaration(newNode) ||
39
+ ts.isTypeOnlyExportDeclaration(newNode)) {
40
+ return newNode;
41
+ }
42
+ return ts.visitEachChild(newNode, (node) => visitNodeChildren(node, newNode, sourceFile, program, context, options), context);
28
43
  }
29
- function visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options) {
44
+ function visitNodeAndReplaceIfNeeded(node, parent, sourceFile, program, context, options) {
30
45
  const ts = options.ts;
31
- if (!ts.isExpression(node) && !ts.isIdentifier(node)) {
32
- return node;
46
+ if (ts.isCallLikeExpression(node)) {
47
+ if (!ts.isExpression(node) ||
48
+ (!options.unsafeHoistFunctionCall &&
49
+ (!options.hoistPureFunctionCall ||
50
+ !hasPureAnnotation(node, sourceFile, ts)))) {
51
+ return node;
52
+ }
33
53
  }
34
- if (ts.isLiteralExpression(node) ||
35
- ts.isObjectLiteralExpression(node) ||
36
- ts.isPrefixUnaryExpression(node) ||
37
- ts.isPostfixUnaryExpression(node) ||
38
- ts.isBinaryExpression(node) ||
39
- ts.isConditionalExpression(node) ||
40
- ts.isVoidExpression(node) ||
41
- ts.isSpreadElement(node) ||
42
- (!options.hoistProperty &&
43
- ts.isPropertyAccessExpression(node) &&
44
- !isEnumAccess(node, program, ts)) ||
45
- (!options.hoistEnumValues &&
46
- ((ts.isPropertyAccessExpression(node) &&
47
- isEnumAccess(node, program, ts)) ||
48
- (ts.isIdentifier(node) && isEnumIdentifier(node, program, ts)))) ||
49
- node.kind === ts.SyntaxKind.TrueKeyword ||
50
- node.kind === ts.SyntaxKind.FalseKeyword ||
51
- node.kind === ts.SyntaxKind.NullKeyword) {
52
- return node;
54
+ else if (ts.isIdentifier(node)) {
55
+ if (!ts.isComputedPropertyName(parent) &&
56
+ ((!ts.isExpression(parent) &&
57
+ (!('initializer' in parent) || node !== parent.initializer)) ||
58
+ (ts.isPropertyAccessExpression(parent) && node === parent.name))) {
59
+ return node;
60
+ }
61
+ if (!options.hoistEnumValues && isEnumIdentifier(node, program, ts)) {
62
+ return node;
63
+ }
64
+ if (!options.hoistUndefinedSymbol &&
65
+ isUndefinedIdentifier(node, parent, program, ts)) {
66
+ return node;
67
+ }
53
68
  }
54
- if (!options.unsafeHoistFunctionCall &&
55
- (!options.hoistPureFunctionCall || !hasPureAnnotation(node, sourceFile, ts))) {
56
- if (ts.isCallLikeExpression(node)) {
69
+ else if (ts.isPropertyAccessExpression(node) ||
70
+ ts.isElementAccessExpression(node)) {
71
+ if (!isHoistablePropertyAccess(node, program, ts)) {
57
72
  return node;
58
73
  }
74
+ if ((!options.hoistProperty && !isEnumAccess(node, program, ts)) ||
75
+ (!options.hoistEnumValues &&
76
+ (isEnumAccess(node, program, ts) ||
77
+ (ts.isIdentifier(node) && isEnumIdentifier(node, program, ts))))) {
78
+ return node;
79
+ }
80
+ }
81
+ else if (ts.isAsExpression(node)) {
82
+ if (!options.unsafeHoistAsExpresion) {
83
+ return node;
84
+ }
85
+ }
86
+ else {
87
+ return node;
59
88
  }
60
89
  if (!options.hoistExternalValues &&
61
90
  isExternalReference(node, program, options.externalNames)) {
62
91
  return node;
63
92
  }
64
- if (ts.isIdentifier(node)) {
65
- if (
66
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
67
- !node.parent ||
68
- (!ts.isExpression(node.parent) &&
69
- (!('initializer' in node.parent) ||
70
- node !== node.parent.initializer)) ||
71
- (ts.isPropertyAccessExpression(node.parent) && node === node.parent.name)) {
72
- return node;
73
- }
74
- }
75
93
  if (!options.unsafeHoistAsExpresion &&
76
94
  (hasAsExpression(node, context, ts) ||
77
- hasParentAsExpression(node.parent, context, ts))) {
95
+ hasParentAsExpression(parent, context, ts))) {
78
96
  return node;
79
97
  }
80
98
  if (!options.unsafeHoistWritableValues) {
@@ -88,6 +106,9 @@ function visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options
88
106
  const type = typeChecker.getTypeAtLocation(node);
89
107
  const flags = type.getFlags();
90
108
  let newNode;
109
+ if (type.isUnionOrIntersection()) {
110
+ return node;
111
+ }
91
112
  if (type.isStringLiteral()) {
92
113
  newNode = context.factory.createStringLiteral(type.value);
93
114
  }
@@ -113,7 +134,12 @@ function visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options
113
134
  newNode = context.factory.createNull();
114
135
  }
115
136
  else if (flags & ts.TypeFlags.Undefined) {
116
- newNode = context.factory.createVoidZero();
137
+ if (options.useUndefinedSymbolForUndefinedValue) {
138
+ newNode = context.factory.createIdentifier('undefined');
139
+ }
140
+ else {
141
+ newNode = context.factory.createVoidZero();
142
+ }
117
143
  }
118
144
  else {
119
145
  return node;
@@ -216,7 +242,8 @@ function hasParentAsExpression(node, context, tsInstance) {
216
242
  if (ts.isAsExpression(node) && !isAsConstExpression(node)) {
217
243
  return true;
218
244
  }
219
- if (ts.isPropertyAccessExpression(node)) {
245
+ if (ts.isPropertyAccessExpression(node) ||
246
+ ts.isElementAccessExpression(node)) {
220
247
  if (hasAsExpression(node.expression, context, ts)) {
221
248
  return true;
222
249
  }
@@ -238,6 +265,13 @@ function hasPureAnnotation(node, sourceFile, tsInstance) {
238
265
  }
239
266
  return false;
240
267
  }
268
+ function getNameFromElementAccessExpression(node, typeChecker) {
269
+ const type = typeChecker.getTypeAtLocation(node.argumentExpression);
270
+ if (type.isStringLiteral() || type.isNumberLiteral()) {
271
+ return `${type.value}`;
272
+ }
273
+ return null;
274
+ }
241
275
  function getMemberName(m, tsInstance) {
242
276
  if (!m || !m.name) {
243
277
  return '';
@@ -259,7 +293,12 @@ function getMemberName(m, tsInstance) {
259
293
  function isReadonlyPropertyAccess(a, typeChecker, tsInstance) {
260
294
  const ts = tsInstance;
261
295
  const type = typeChecker.getTypeAtLocation(a.expression);
262
- const memberName = a.name.getText();
296
+ const memberName = ts.isPropertyAccessExpression(a)
297
+ ? a.name.getText()
298
+ : getNameFromElementAccessExpression(a, typeChecker);
299
+ if (memberName == null) {
300
+ return false;
301
+ }
263
302
  if (type.getFlags() & ts.TypeFlags.Object) {
264
303
  const dummyTypeNode = typeChecker.typeToTypeNode(type, a, ts.NodeBuilderFlags.NoTruncation);
265
304
  if (dummyTypeNode && ts.isTypeLiteralNode(dummyTypeNode)) {
@@ -309,7 +348,8 @@ function isReadonlyExpression(node, program, tsInstance) {
309
348
  }
310
349
  }
311
350
  }
312
- if (ts.isPropertyAccessExpression(node)) {
351
+ if (ts.isPropertyAccessExpression(node) ||
352
+ ts.isElementAccessExpression(node)) {
313
353
  if (isEnumAccess(node, program, ts)) {
314
354
  return true;
315
355
  }
@@ -317,6 +357,42 @@ function isReadonlyExpression(node, program, tsInstance) {
317
357
  }
318
358
  return null;
319
359
  }
360
+ function isHoistablePropertyAccess(a, program, tsInstance) {
361
+ const ts = tsInstance;
362
+ const typeChecker = program.getTypeChecker();
363
+ const type = typeChecker.getTypeAtLocation(a.expression);
364
+ const memberName = ts.isPropertyAccessExpression(a)
365
+ ? a.name.getText()
366
+ : getNameFromElementAccessExpression(a, typeChecker);
367
+ if (memberName == null) {
368
+ return false;
369
+ }
370
+ if (type.getFlags() & ts.TypeFlags.Object) {
371
+ const prop = type.getProperty(memberName);
372
+ // If the property access uses indexed access, `prop` will be undefined
373
+ if (prop) {
374
+ return true;
375
+ }
376
+ }
377
+ return false;
378
+ }
379
+ function isUndefinedIdentifier(node, parent, program, tsInstance) {
380
+ if (tsInstance.isPropertyAccessExpression(parent) ||
381
+ tsInstance.isElementAccessExpression(parent)) {
382
+ return false;
383
+ }
384
+ const typeChecker = program.getTypeChecker();
385
+ const type = typeChecker.getTypeAtLocation(node);
386
+ const sym = typeChecker.getSymbolAtLocation(node);
387
+ if (!sym || sym.getEscapedName().toString() !== 'undefined') {
388
+ return false;
389
+ }
390
+ if (type.isUnionOrIntersection() ||
391
+ !(type.getFlags() & ts.TypeFlags.Undefined)) {
392
+ return false;
393
+ }
394
+ return true;
395
+ }
320
396
  ////////////////////////////////////////////////////////////////////////////////
321
397
  export function printSource(sourceFile) {
322
398
  return printSourceImpl(sourceFile)[0];
@@ -1,2 +1,2 @@
1
- declare const _default: "0.4.0";
1
+ declare const _default: "0.5.0";
2
2
  export default _default;
package/dist/version.mjs CHANGED
@@ -1 +1 @@
1
- export default '0.4.0';
1
+ export default '0.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-const-value-transformer",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "engines": {
5
5
  "node": ">=20.19.3"
6
6
  },
@@ -23,6 +23,16 @@
23
23
  "import": "./dist/createTransformer.mjs",
24
24
  "types": "./dist/createTransformer.d.mts"
25
25
  },
26
+ "./createPortalTransformer": {
27
+ "default": "./dist/createPortalTransformer.mjs",
28
+ "import": "./dist/createPortalTransformer.mjs",
29
+ "types": "./dist/createPortalTransformer.d.mts"
30
+ },
31
+ "./createPortalTransformer.mjs": {
32
+ "default": "./dist/createPortalTransformer.mjs",
33
+ "import": "./dist/createPortalTransformer.mjs",
34
+ "types": "./dist/createPortalTransformer.d.mts"
35
+ },
26
36
  "./getCustomTransformers": {
27
37
  "default": "./dist/getCustomTransformers.mjs",
28
38
  "import": "./dist/getCustomTransformers.mjs",