Repository: danvk/literate-ts (ts)

Issue:
Support string enum values
As it turns out, #230 is because `compilerOptions` expects number values for enums, not string values. It would be nice if you could write `target=ES2022` instead of `target=9`, though.

Pull request description:
feat: allow named values for tsconfig enums
Fixes #249 

Because "target: ES2017" is much clearer than "target: 4"!

It would be nice if this reported a failure rather than crashing.

Patch:
diff --git a/src/code-sample.ts b/src/code-sample.ts
index 5d70a40..19e0c22 100644
--- a/src/code-sample.ts
+++ b/src/code-sample.ts
@@ -6,6 +6,8 @@ import {extractAsciidocSamples} from './asciidoc.js';
 import {extractMarkdownSamples} from './markdown.js';
 import {generateIdMetadata} from './metadata.js';
 import {stripComments} from 'jsonc-parser';
+import ts from 'typescript';
+import {EnumType, getEnumValue} from './utils.js';
 
 export interface Processor {
   /** Let the processor know about the current line number (0-based). */
@@ -20,6 +22,15 @@ export interface Processor {
   resetWithNormalLine(): void;
 }
 
+const tsconfigToEnum: Record<string, EnumType<object> | undefined> = {
+  jsx: ts.JsxEmit,
+  target: ts.ScriptTarget,
+  module: ts.ModuleKind,
+  moduleResolution: ts.ModuleResolutionKind,
+  moduleDetection: ts.ModuleDetectionKind,
+  newLine: ts.NewLineKind,
+};
+
 function process(
   text: string,
   slug: string,
@@ -86,8 +97,10 @@ function process(
         skipRemaining = true;
       } else if (directive.startsWith('tsconfig:')) {
         const [key, value] = directive.split(':', 2)[1].split('=', 2);
-        tsOptions[key] =
-          value === 'true'
+        const enumKind = tsconfigToEnum[key];
+        tsOptions[key] = enumKind
+          ? getEnumValue(key, enumKind, value)
+          : value === 'true'
             ? true
             : value === 'false'
               ? false
diff --git a/src/utils.ts b/src/utils.ts
index c3b005a..2adbdea 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -95,3 +95,27 @@ export const tuple = <Args extends unknown[]>(...args: Args): Args => args;
 export function reduceIndentation(src: string): string {
   return src.replace(/^(?:    )+/gm, spaces => _.repeat(' ', spaces.length / 2));
 }
+
+// See https://stackoverflow.com/a/50159864/388951
+export type EnumType<E> = Record<keyof E, number | string> & {[k: number]: string};
+
+export function getEnumValue<E extends EnumType<E>>(
+  enumName: string,
+  enumType: E,
+  str: string | number,
+): number {
+  if (!(str in enumType)) {
+    throw new Error(
+      `${str} is not a valid ${enumName}. Expected one of: ${Object.keys(enumType).join(', ')}`,
+    );
+  }
+
+  if (typeof str === 'number') {
+    return str;
+  } else if (!isNaN(Number(str))) {
+    return Number(str);
+  } else {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    return (enumType as any)[str];
+  }
+}


Test patch:
diff --git a/src/test/inputs/check-emit.asciidoc b/src/test/inputs/check-emit.asciidoc
index 41181fd..de6d860 100644
--- a/src/test/inputs/check-emit.asciidoc
+++ b/src/test/inputs/check-emit.asciidoc
@@ -1,9 +1,9 @@
 With target=ES2021, numeric separators are preserved:
 
 // verifier:tsconfig:alwaysStrict=false
-// verifier:tsconfig:moduleResolution=2
-// verifier:tsconfig:module=99
-// verifier:tsconfig:target=8
+// verifier:tsconfig:moduleResolution=NodeJs
+// verifier:tsconfig:module=ESNext
+// verifier:tsconfig:target=ES2021
 [[sep-es2021]]
 [source,ts]
 ----
@@ -20,7 +20,7 @@ const num = 1_234_567;
 
 With a lower target, however, they are not:
 
-// verifier:tsconfig:target=1
+// verifier:tsconfig:target=ES5
 [[sep-es5]]
 [source,ts]
 ----
diff --git a/src/test/inputs/top-level-await.asciidoc b/src/test/inputs/top-level-await.asciidoc
index a4dbcc3..8d5eb96 100644
--- a/src/test/inputs/top-level-await.asciidoc
+++ b/src/test/inputs/top-level-await.asciidoc
@@ -1,8 +1,8 @@
 This code sample uses top-level `await`:
 
-// verifier:tsconfig:moduleResolution=2
-// verifier:tsconfig:module=7
-// verifier:tsconfig:target=4
+// verifier:tsconfig:moduleResolution=NodeJs
+// verifier:tsconfig:module=ES2022
+// verifier:tsconfig:target=ES2017
 [source,ts]
 ----
 declare function get(url: string): Promise<number>;
diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts
index ffb23c8..f7077fa 100644
--- a/src/test/utils.test.ts
+++ b/src/test/utils.test.ts
@@ -1,4 +1,6 @@
-import {dedent, matchAndExtract, reduceIndentation, sha256} from '../utils.js';
+import ts from 'typescript';
+
+import {dedent, getEnumValue, matchAndExtract, reduceIndentation, sha256} from '../utils.js';
 
 describe('utils', () => {
   test('matchAndExtract', () => {
@@ -31,4 +33,16 @@ describe('utils', () => {
     }
   `);
   });
+
+  test('getEnumValue', () => {
+    expect(getEnumValue('ScriptTarget', ts.ScriptTarget, 2)).toEqual(2);
+    expect(getEnumValue('ScriptTarget', ts.ScriptTarget, '2')).toEqual(2);
+    expect(getEnumValue('ScriptTarget', ts.ScriptTarget, 'ES2020')).toEqual(7);
+    expect(() => getEnumValue('ScriptTarget', ts.ScriptTarget, 'ES2007')).toThrow(
+      /ES2007 is not a valid ScriptTarget. Expected one of: 0, 1, 2, 3, 4, 5, 6, 7/,
+    );
+    expect(() => getEnumValue('ScriptTarget', ts.ScriptTarget, 2007)).toThrow(
+      /2007 is not a valid ScriptTarget. Expected one of: 0, 1, 2, 3, 4, 5, 6, 7/,
+    );
+  });
 });