Repository: karen-irc/option-t
Pull request #1525 (2023-01-19 22:12:55)

Fix the order of `types` condition in exports field in package.json
Retry #1522

---

By the blog,
https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#package-json-exports-imports-and-self-referencing

We need do these things:

1. `types` condition should always come first.
2. `import` and `require` should be condition object.


Patch:
diff --git a/tools/package_json_rewriter/transformer/add_exports_field/ExportEntry.mjs b/tools/package_json_rewriter/transformer/add_exports_field/ExportEntry.mjs
index e70a3794..17d78691 100644
--- a/tools/package_json_rewriter/transformer/add_exports_field/ExportEntry.mjs
+++ b/tools/package_json_rewriter/transformer/add_exports_field/ExportEntry.mjs
@@ -15,7 +15,7 @@ class AbstractExportEntry {
 
 const EXTENSION_CJS = 'cjs';
 const EXTENSION_MJS = 'js';
-const EXTENSION_DTS = 'd.ts';
+const EXTENSION_DMTS = 'd.ts';
 const EXTENSION_DCTS = 'd.cts';
 
 export class ExportEntry extends AbstractExportEntry {
@@ -49,11 +49,13 @@ export class ExportEntry extends AbstractExportEntry {
 
         const esm = `./esm/${p}.${EXTENSION_MJS}`;
         const cjs = `./cjs/${p}.${EXTENSION_CJS}`;
-        const dts = `./esm/${p}.${EXTENSION_DTS}`;
+        const dmts = `./esm/${p}.${EXTENSION_DMTS}`;
+        const dcts = `./cjs/${p}.${EXTENSION_DCTS}`;
         const entry = constructDualPackagePathValue({
             esm,
             cjs,
-            dts,
+            dmts,
+            dcts,
         });
         return entry;
     }
@@ -135,7 +137,7 @@ class ESModuleCompatFileExport extends CompatExportEntry {
 
     _toExportEntry() {
         const key = this.key();
-        const dts = `${key}.${EXTENSION_DTS}`;
+        const dts = `${key}.${EXTENSION_DMTS}`;
 
         const p = `${key}.${EXTENSION_MJS}`;
         const value = constructPathValue({
@@ -164,16 +166,18 @@ class LibCompatFileExport extends CompatExportEntry {
         const esmKey = key.replace(LIB_PATH_PREFIX, ESM_PATH_PREFIX);
         assert.ok(esmKey.startsWith(ESM_PATH_PREFIX));
         const esm = `${esmKey}.${EXTENSION_MJS}`;
-        const dts = `${esmKey}.${EXTENSION_DTS}`;
+        const dmts = `${esmKey}.${EXTENSION_DMTS}`;
 
         const cjsKey = key.replace(LIB_PATH_PREFIX, CJS_PATH_PREFIX);
         assert.ok(cjsKey.startsWith(CJS_PATH_PREFIX));
         const cjs = `${cjsKey}.cjs`;
+        const dcts = `${cjsKey}.${EXTENSION_DCTS}`;
 
         const p = constructDualPackagePathValue({
             cjs,
             esm,
-            dts,
+            dmts,
+            dcts,
         });
         return p;
     }
@@ -227,7 +231,7 @@ export class CommonJSCompatDirExport extends AbstractCompatDirExport {
 export class ESModuleCompatDirExport extends AbstractCompatDirExport {
     constructor(dirpath) {
         const p = `esm/${dirpath}`;
-        super(p, EXTENSION_MJS, EXTENSION_DTS);
+        super(p, EXTENSION_MJS, EXTENSION_DMTS);
         Object.freeze(this);
     }
 }
@@ -255,16 +259,18 @@ export class LibCompatDirExport extends AbstractExportEntry {
         const esmKey = actualPath.replace(LIB_PATH_PREFIX, ESM_PATH_PREFIX);
         assert.ok(esmKey.startsWith(ESM_PATH_PREFIX));
         const mjs = `${esmKey}.${EXTENSION_MJS}`;
-        const dts = `${esmKey}.${EXTENSION_DTS}`;
+        const dmts = `${esmKey}.${EXTENSION_DMTS}`;
 
         const cjsKey = actualPath.replace(LIB_PATH_PREFIX, CJS_PATH_PREFIX);
         assert.ok(cjsKey.startsWith(CJS_PATH_PREFIX));
         const cjs = `${cjsKey}.${EXTENSION_CJS}`;
+        const dcts = `${cjsKey}.${EXTENSION_DCTS}`;
 
         const value = constructDualPackagePathValue({
             cjs,
             esm: mjs,
-            dts,
+            dmts,
+            dcts
         });
         return value;
     }
@@ -274,25 +280,34 @@ export class LibCompatDirExport extends AbstractExportEntry {
     }
 }
 
-function constructDualPackagePathValue({ cjs, esm, dts }) {
+function constructDualPackagePathValue({ cjs, esm, dmts, dcts }) {
     assert.strictEqual(typeof cjs, 'string', 'cjs should be string');
     assert.strictEqual(typeof esm, 'string', 'esm should be string');
-    assert.strictEqual(typeof dts, 'string', 'dts should be string');
+    assert.strictEqual(typeof dmts, 'string', 'dmts should be string');
+    assert.strictEqual(typeof dcts, 'string', 'dcts should be string');
+
+    const importCondition = constructPathValue({
+        filepath: esm,
+        dts: dmts,
+    });
+
+    const requireCondition = constructPathValue({
+        filepath: cjs,
+        dts: dcts,
+    });
 
     // [By the document of Node.js v14.2](https://nodejs.org/api/esm.html#esm_resolution_algorithm),
     //  * Condition matching is applied in object order from first to last within the "exports" object.
     //  * `["node", "import"]` is used as _defaultEnv_ for its ES Module resolver.
     //
     // see also https://nodejs.org/api/esm.html#esm_conditional_exports
-    return {
-        'import': esm,
-        'require': cjs,
-        // https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#packagejson-exports-imports-and-self-referencing
-        'types': dts,
+    return Object.freeze({
+        'import': importCondition,
+        'require':  requireCondition,
         // _default_ should be placed to the last.
         // https://nodejs.org/api/esm.html#esm_conditional_exports
         'default': esm,
-    };
+    });
 }
 
 function constructPathValue({ filepath, dts }) {
@@ -304,11 +319,12 @@ function constructPathValue({ filepath, dts }) {
     //  * `["node", "import"]` is used as _defaultEnv_ for its ES Module resolver.
     //
     // see also https://nodejs.org/api/esm.html#esm_conditional_exports
-    return {
-        // https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#packagejson-exports-imports-and-self-referencing
+    return Object.freeze({
+        // > Note that the "types" condition should always come first in "exports".
+        // https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#package-json-exports-imports-and-self-referencing
         'types': dts,
         // _default_ should be placed to the last.
         // https://nodejs.org/api/esm.html#esm_conditional_exports
         'default': filepath,
-    };
+    });
 }


Test patch:
diff --git a/tools/test_package_json_exports_field_format.mjs b/tools/test_package_json_exports_field_format.mjs
index 4b4a70e0..221a41da 100644
--- a/tools/test_package_json_exports_field_format.mjs
+++ b/tools/test_package_json_exports_field_format.mjs
@@ -63,6 +63,72 @@ async function checkWhetherFileExist(parentDir, filename) {
     assert.ok(stat.isFile(), `${filename} must be file`);
 }
 
+const TestMode = Object.freeze({
+    CJS: 'commonjs',
+    ESM: 'es_module',
+});
+
+const EXT_ES_MODULE = '.js';
+const EXT_COMMONJS = '.cjs';
+const EX_D_TS = '.d.ts';
+const EX_D_CTS = '.d.cts';
+
+async function testBasicConditionObject(mode, t, conditionValue) {
+    await t.test('should be condition object', (_t) => {
+        assert.strictEqual(typeof conditionValue, 'object');
+        assert.notStrictEqual(conditionValue, null);
+    });
+
+    await t.test('contains expected fields', (_t) => {
+        const keys = Object.keys(conditionValue);
+        assert.deepStrictEqual(keys, ['types', 'default']);
+    });
+
+    const valueDefault = conditionValue.default;
+    const valueTypes = conditionValue.types;
+
+    switch (mode) {
+        case TestMode.ESM:
+            await t.test(`check 'default' file name style is ${EXT_ES_MODULE}`, (_t) => {
+                assert.ok(valueDefault.endsWith(EXT_ES_MODULE));
+            });
+            await t.test(`check 'types' file name style is ${EX_D_TS}`, (_t) => {
+                assert.ok(valueTypes.endsWith(EX_D_TS));
+            });
+            break;
+        case TestMode.CJS:
+            await t.test(`check 'default' file name style is ${EXT_COMMONJS}`, (_t) => {
+                assert.ok(valueDefault.endsWith(EXT_COMMONJS));
+            });
+            await t.test(`check 'types' file name style is ${EX_D_CTS}`, (_t) => {
+                assert.ok(valueTypes.endsWith(EX_D_CTS));
+            });
+            break;
+        default:
+            throw new RangeError(`unknown mode: ${mode}`);
+    }
+}
+
+async function testComplexConditionObject(t, conditionValue) {
+    await t.test('should be condition object', (_t) => {
+        assert.strictEqual(typeof conditionValue, 'object');
+        assert.notStrictEqual(conditionValue, null);
+    });
+
+    await t.test('`import` field is condition', async (t) => {
+        await testBasicConditionObject(TestMode.ESM, t, conditionValue.import);
+    });
+
+    await t.test('`require` field is condition', async (t) => {
+        await testBasicConditionObject(TestMode.CJS, t, conditionValue.require);
+    });
+
+    await t.test('contains expected fields', (_t) => {
+        const keys = Object.keys(conditionValue);
+        assert.deepStrictEqual(keys, ['import', 'require', 'default']);
+    });
+}
+
 await test(`Check package.json's 'exports' field format`, async (t) => {
     const targetDir = process.env.TARGET_DIR;
     assert.ok(!!targetDir, 'TARGET_DIR envvar should be set');
@@ -82,4 +148,22 @@ await test(`Check package.json's 'exports' field format`, async (t) => {
         }
         await Promise.all(runningTests);
     });
+
+    await t.test(`check condition object's format`, async (t) => {
+        const runningTests = [];
+        for (const [exportedPath, value] of Object.entries(exports)) {
+            const running = t.test(exportedPath, async (t) => {
+                if (exportedPath.startsWith('./cjs/')) {
+                    await testBasicConditionObject(TestMode.CJS, t, value);
+                } else if (exportedPath.startsWith('./esm/')) {
+                    await testBasicConditionObject(TestMode.ESM, t, value);
+                } else {
+                    await testComplexConditionObject(t, value);
+                }
+            });
+
+            runningTests.push(running);
+        }
+        await Promise.all(runningTests);
+    });
 });