Unimport 源码解读(1): toImports 的实现

148 阅读4分钟

1. 通过什么来了解源码的功能?

我通常是通过源码中的测试来了解功能点,测试代码完全可以看做是一个活文档(后续准备些一篇关于测试的好处的文章),比如:

it('basic', () => {
    const imports: Import[] = [{ from: 'test-id', name: 'fooBar', as: 'fooBar' }]
    expect(toImports(imports))
      .toMatchInlineSnapshot('"import { fooBar } from \'test-id\';"')
    expect(toImports(imports, true))
      .toMatchInlineSnapshot('"const { fooBar } = require(\'test-id\');"')
  })

根据这个测试代码,我们大致知道toImports需要实现的功能:

  • toImports 接收两个参数
    • 类型是Import的一个数组
    • isCJS是不是commonjs的标志位
  • 输出内容:
    • 根据是否是commonjs分别将输入为Import类型的数组转换为相应的引入语句

2. 实现功能的过程

根据测试文件可以知道,我们实现的功能需要跑通测试文件下的所有case,那么我们也就完成了相应功能,所以我们就可以通过增加一个一个的测试用例,来完整的实现这个过程,这个过程也叫做TDD.

  1. 实现基础功能

    1. 上测试代码

      it('basic', () => {
      const imports: Import[] = [{ from: 'test-id', name: 'fooBar', as: 'fooBar' }]
      expect(toImports(imports))
        .toMatchInlineSnapshot('"import { fooBar } from \'test-id\';"')
      expect(toImports(imports, true))
        .toMatchInlineSnapshot('"const { fooBar } = require(\'test-id\');"')
      })
      
    2. 实现功能

      // 
      type Import {
          from: string
          name: string
          as: string
      }
      export function toImports (imports: Import[], isCJS = false) {
        return imports.map((importObj) => {
          return isCJS
            ? `const { ${importObj.name} } = require('${importObj.from}');`
            : `import { ${importObj.name} } from '${importObj.from}';`
        }).join('/n')
      }
      
  2. 增加别名功能

    1. 加一段测试代码

        it('alias', () => {
          const imports: Import[] = [{ from: 'test-id', name: 'foo', as: 'bar' }]
          expect(toImports(imports))
            .toMatchInlineSnapshot('"import { foo as bar } from \'test-id\';"')
          expect(toImports(imports, true))
            .toMatchInlineSnapshot('"const { foo: bar } = require(\'test-id\');"')
        })
      
    2. 实现功能

      分析:as作为别名,name作为模块导出的名称,所以只有当asname才不需要生成语句的as后面的名称

      // 
      type Import {
          from: string
          name: string
          as: string
      }
      // 增加一个函数
      // 导出的名称分为 as 和 name 相等以及不相等的情况
      function stringifyImportAlias (item: Import, isCJS = false) {
        return item.name === item.as
          ? item.name
          : isCJS
            ? `${item.name}: ${item.as}`
            : `${item.name} as ${item.as}`
      }
      
      export function toImports (imports: Import[], isCJS = false) {
        return imports.map((importObj) => {
          return isCJS
            ? `const { ${stringifyImportAlias(importObj)} } = require('${importObj.from}');`
            : `import { ${stringifyImportAlias(importObj)} } from '${importObj.from}';`
        }).join('/n')
      }
      
  3. 如果引入中有多个相同的from

    1. 增加测试代码

      it('multiple', () => {
       const imports: Import[] = [
         { from: 'test1', name: 'foo', as: 'foo' },
         { from: 'test1', name: 'bar', as: 'bar' },
         { from: 'test2', name: 'foobar', as: 'foobar' }
       ]
       expect(toImports(imports))
         .toMatchInlineSnapshot(`
           "import { foo, bar } from 'test1';
           import { foobar } from 'test2';"
         `)
       expect(toImports(imports, true))
         .toMatchInlineSnapshot(`
           "const { foo, bar } = require('test1');
           const { foobar } = require('test2');"
         `)
        })
      
    2. 实现功能

      分析:from可以有多个,最后可以转换为import {foo1, foo2} from 'bar',所以我们想通过对象的结构:keyfrom的值,valuefrom相同的值的一个set集合。

      function stringifyImportAlias (item: Import, isCJS = false) {
           return item.name === item.as
             ? item.name
             : isCJS
               ? `${item.name}: ${item.as}`
               : `${item.name} as ${item.as}`
         }
      
         export function toImportModuleMap (imports: Import[]) {
           const map: Record<Import['from'], Set<Import>> = {}
           for (const _import of imports) {
             if (!map[_import.from]) {
               map[_import.from] = new Set()
             }
             map[_import.from].add(_import)
           }
           return map
         }
      
         export function toImports (imports: Import[], isCJS = false) {
           // 转换为分析的对象形式
           const map = toImportModuleMap(imports)
           // 遍历返回
           return Object.entries(map).map(([name, importSet]) => {
             return isCJS
               ? `const { ${Array.from(importSet).map(importObj => stringifyImportAlias(importObj)).join(', ')} } = require('${name}');`
               : `import { ${Array.from(importSet).map(importObj => stringifyImportAlias(importObj)).join(', ')} } from '${name}';`
           }).join('\n')
         }
      
      
  4. 增加 defult

    1. 增加测试的代码

        it('default', () => {
          const imports: Import[] = [
            { from: 'test1', name: 'default', as: 'foo' }
          ]
          expect(toImports(imports))
            .toMatchInlineSnapshot('"import foo from \'test1\';"')
          expect(toImports(imports, true))
            .toMatchInlineSnapshot('"const { default: foo } = require(\'test1\');"')
        })
      
    2. 功能实现

      分析:将namedefault的进行单独处理

      function stringifyImportAlias (item: Import, isCJS = false) {
        return item.name === item.as
          ? item.name
          : isCJS
            ? `${item.name}: ${item.as}`
            : `${item.name} as ${item.as}`
      }
      
      export function toImportModuleMap (imports: Import[]) {
        const map: Record<Import['from'], Set<Import>> = {}
        for (const _import of imports) {
          if (!map[_import.from]) {
            map[_import.from] = new Set()
          }
          map[_import.from].add(_import)
        }
        return map
      }
      
      export function toImports (imports: Import[], isCJS = false) {
        const map = toImportModuleMap1(imports)
        return Object.entries(map).map(([name, importSet]) => {
          const entries:string [] = []
          const _imports = Array.from(importSet).filter((i) => {
            if (i.name === 'default') {
              entries.push(
                isCJS
                  ? `const { default: ${i.as} } = require('${name}');`
                  : `import ${i.as} from '${name}';`
              )
              return false
            }
            return true
          })
          if (_imports.length) {
            const importsAs = _imports.map(importObj => stringifyImportAlias(importObj))
            entries.push(
              isCJS
                ? `const { ${importsAs.join(', ')} } = require('${name}');`
                : `import { ${importsAs.join(', ')} } from '${name}';`
            )
          }
          return entries
        }).join('\n')
      }
      
  5. 增加*

    1. 增加测试用例

        it('import all as', () => {
          const imports: Import[] = [
            { from: 'test1', name: '*', as: 'foo' }
          ]
          expect(toImports(imports))
            .toMatchInlineSnapshot('"import * as foo from \'test1\';"')
          expect(toImports(imports, true))
            .toMatchInlineSnapshot('"const foo = require(\'test1\');"')
        })
      
    2. 功能实现

      分析:将*单独处理

      export function toImports (imports: Import[], isCJS = false) {
        const map = toImportModuleMap(imports)
        return Object.entries(map).map(([name, importSet]) => {
          const entries:string [] = []
          const _imports = Array.from(importSet).filter((i) => {
            if (i.name === 'default') {
              entries.push(
                isCJS
                  ? `const { default: ${i.as} } = require('${name}');`
                  : `import ${i.as} from '${name}';`
              )
              return false
            } else if (i.name === '*') {
            // 增加 * 的单独处理
              entries.push(
                isCJS
                  ? `const  ${i.as}  = require('${name}');`
                  : `import * as ${i.as} from '${name}';`
              )
              return false
            }
            return true
          })
          if (_imports.length) {
            const importsAs = _imports.map(importObj => stringifyImportAlias(importObj))
            entries.push(
              isCJS
                ? `const { ${importsAs.join(', ')} } = require('${name}');`
                : `import { ${importsAs.join(', ')} } from '${name}';`
            )
          }
          return entries
        }).join('\n')
      }
      
  6. 其他边界处理

    1. 增加测试用例

      it('mixed', () => {
          const imports: Import[] = [
            { from: 'test1', name: '*', as: 'foo' },
            { from: 'test1', name: '*', as: 'bar' },
            { from: 'test1', name: 'foo', as: 'foo' },
            { from: 'test1', name: 'bar', as: 'bar' },
            { from: 'test2', name: 'foobar', as: 'foobar' },
            { from: 'test2', name: 'default', as: 'defaultAlias' },
            { from: 'sideeffects', name: '', as: '' }
          ]
          expect(toImports(imports))
            .toMatchInlineSnapshot(`
              "import * as foo from 'test1';
              import * as bar from 'test1';
              import { foo, bar } from 'test1';
              import defaultAlias from 'test2';
              import { foobar } from 'test2';
              import 'sideeffects';"
            `)
          expect(toImports(imports, true))
            .toMatchInlineSnapshot(`
              "const foo = require('test1');
              const bar = require('test1');
              const { foo, bar } = require('test1');
              const { default: defaultAlias } = require('test2');
              const { foobar } = require('test2');
              require('sideeffects');"
            `)
          })
      
    2. 功能实现

      分析:增加name或者as为空的条件

      export function toImports (imports: Import[], isCJS = false) {
        const map = toImportModuleMap(imports)
        return Object.entries(map).flatMap(([name, importSet]) => {
          const entries:string [] = []
          const _imports = Array.from(importSet).filter((i) => {
            if (i.name === '' || i.as === '') {
              entries.push(
                isCJS
                  ? `require('${name}');`
                  : `import '${name}';`
              )
              return false
            } else if (i.name === 'default') {
              entries.push(
                isCJS
                  ? `const { default: ${i.as} } = require('${name}');`
                  : `import ${i.as} from '${name}';`
              )
              return false
            } else if (i.name === '*') {
              entries.push(
                isCJS
                  ? `const  ${i.as}  = require('${name}');`
                  : `import * as ${i.as} from '${name}';`
              )
              return false
            }
            return true
          })
          if (_imports.length) {
            const importsAs = _imports.map(importObj => stringifyImportAlias(importObj))
            entries.push(
              isCJS
                ? `const { ${importsAs.join(', ')} } = require('${name}');`
                : `import { ${importsAs.join(', ')} } from '${name}';`
            )
          }
          return entries
        }).join('\n')
      }
      

3.总结

以上我们便是通过TDD的思想实现toImports函数,我们可以通过测试文件知道函数的意图,然后通过一个一个的case的通过来实现功能。只要将所有的case通过那么我们的功能函数就是我们期待的