-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: generate correct rewritten import statements
- Loading branch information
Showing
7 changed files
with
171 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,32 @@ | ||
import type { ModuleMap, Options } from '../types' | ||
import type { Context, ModuleMap, Options } from '../types'; | ||
|
||
const CDN_HOST = 'https://cdn.jsdelivr.net' | ||
const CDN_HOST = 'https://cdn.jsdelivr.net'; | ||
|
||
const createContext = (options?: Options) => { | ||
const createContext = (options?: Options): Context => { | ||
const modulesMap: ModuleMap = new Map(); | ||
const cwd = options?.cwd ?? process.cwd(); | ||
|
||
if (options?.modules) { | ||
for (const { module, transform } of options.modules) { | ||
// If no transform given, use full ESM lib | ||
if (typeof transform === 'function') { | ||
modulesMap.set(module, transform) | ||
modulesMap.set(module, transform); | ||
} else { | ||
modulesMap.set(module, module); | ||
} | ||
} | ||
} | ||
|
||
const endpoint = options?.endpoint ?? 'npm' | ||
const endpoint = options?.endpoint ?? 'npm'; | ||
|
||
return { | ||
// The modules that are being processed. Set all to external if options.modules === true | ||
modules: modulesMap, | ||
// The current working directory. | ||
cwd: options?.cwd ?? process.cwd(), | ||
cwd, | ||
// The endpoint that is being used. | ||
host: `${CDN_HOST}/${endpoint}`, | ||
} | ||
} | ||
}; | ||
}; | ||
|
||
export { CDN_HOST, createContext } | ||
export { CDN_HOST, createContext }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,70 @@ | ||
import { init, parse } from 'es-module-lexer' | ||
import { init, parse } from 'es-module-lexer'; | ||
import MagicString from 'magic-string'; | ||
|
||
import { ModuleMap, TransformFunction } from '../types'; | ||
|
||
type ImportTuple = [OriginalImport: string, RenamedImport: string]; | ||
|
||
const generateImportTuple = (importElem: string): ImportTuple[] => { | ||
const tuple: ImportTuple[] = []; | ||
import { Context, ImportStatementTuple, ImportTuple, ModuleMapReturn } from '../types'; | ||
import { getVersion } from './version'; | ||
|
||
const generateImportTuple = (importElem: string): ImportTuple => { | ||
const importRename = importElem.split(' as ').map(e => e.trim()); | ||
if (importRename.length === 1) | ||
tuple.push([importRename[0], importRename[0]]); // 'map' into ['map', 'map'] | ||
else | ||
tuple.push([importRename[0], importRename[1]]); // 'merge as LodashMerge' into ['merge', 'LodashMerge'] | ||
return [importRename[0], importRename[0]]; // 'map' into ['map', 'map'] | ||
|
||
return tuple | ||
} | ||
return [importRename[0], importRename[1]]; // 'merge as LodashMerge' into ['merge', 'LodashMerge'] | ||
}; | ||
|
||
const generateImportStatements = (importModule: string, importTuples: ImportTuple[], transform: TransformFunction | string): string => { | ||
const importStatements = importTuples.map(importTuple => { | ||
const [originalImport, renamedImport] = importTuple; | ||
const newModulePath = typeof transform === 'string' ? transform : transform(importModule, originalImport); | ||
return `import ${renamedImport} from '${newModulePath}'`; | ||
}).join('\n'); | ||
return importStatements; | ||
} | ||
const generateImportStatement = async (importModule: string, importTuple: ImportTuple, transform: ModuleMapReturn, ctx: Context): Promise<string> => { | ||
const version = await getVersion(importModule, ctx.cwd); | ||
const [originalImport, renamedImport] = importTuple; | ||
const transformedModule = typeof transform === 'string' ? transform : transform(`${importModule}@${version}`, originalImport); | ||
const newModulePath = `${ctx.host}/${transformedModule}/+esm`; | ||
return `import ${renamedImport} from '${newModulePath}';`; | ||
}; | ||
|
||
const updateCode = (code: string, importStatements: ImportStatementTuple[]) => { | ||
const magicCode = new MagicString(code); | ||
// New rewritten statements will be shorter or longer offsetting indexes for other imports | ||
let offset = 0; | ||
for (const importStatementTuple of importStatements) { | ||
const [importStatement, statementStart, statementEnd] = importStatementTuple; | ||
magicCode.remove(statementStart + offset, statementEnd + offset); | ||
magicCode.appendLeft(statementStart + offset, importStatement); | ||
offset += importStatement.length - (statementEnd - statementStart); | ||
} | ||
|
||
return magicCode.toString(); | ||
}; | ||
|
||
const transformImports = async (code: string, modules: ModuleMap) => { | ||
const transformImports = async (code: string, ctx: Context) => { | ||
await init; | ||
|
||
const [imports] = parse(code); | ||
const magicCode = new MagicString(code); | ||
const importStatements: ImportStatementTuple[] = []; | ||
|
||
for (const importSpecifier of imports) { | ||
const importModule = importSpecifier.n; // e.g. 'lodash' | ||
if (importModule === undefined) | ||
throw new Error(`Bad import specifier: ${importSpecifier}`); | ||
|
||
// If module isn't included, skip to next importSpecifier | ||
const transformFunction = modules.get(importModule); | ||
const transformFunction = ctx.modules.get(importModule); | ||
if (transformFunction !== undefined) { | ||
// e.g. import { map, merge as LodashMerge } from "lodash" | ||
const importStatement = code.slice(importSpecifier.ss, importSpecifier.se); | ||
// Returns an array of import consts e.g. ['map', 'merge as LodashMerge'] | ||
const importElems = importStatement.slice(importStatement.indexOf('{') + 1, importStatement.indexOf('}')).split(','); | ||
|
||
// Setup import tuple | ||
const newImportStatements: string[] = []; | ||
for (const importElem of importElems) { | ||
const importTuples = generateImportTuple(importElem); | ||
|
||
const importTuple = generateImportTuple(importElem); | ||
// Generate rewritten import statements | ||
const newImportStatements = generateImportStatements(importModule, importTuples, transformFunction); | ||
console.log(newImportStatements) | ||
magicCode.overwrite(importSpecifier.ss, importSpecifier.se, newImportStatements); | ||
|
||
console.log(magicCode.toString()) | ||
// eslint-disable-next-line no-await-in-loop | ||
newImportStatements.push(await generateImportStatement(importModule, importTuple, transformFunction, ctx)); | ||
} | ||
importStatements.push([newImportStatements.join('\n'), importSpecifier.ss, importSpecifier.se]); | ||
} | ||
} | ||
return code; | ||
} | ||
return updateCode(code, await Promise.all(importStatements)); | ||
}; | ||
|
||
export { generateImportTuple, transformImports } | ||
export { generateImportStatement, generateImportTuple, transformImports, updateCode }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,71 @@ | ||
import { describe, expect, it } from "vitest"; | ||
|
||
import { transformImports } from "../../src/core/transform"; | ||
|
||
describe.skip("Transform", () => { | ||
const code = ` | ||
import { import1, import2 as import3 } from 'test-package'; | ||
import { importNested } from 'test-package/nested'; | ||
import defaultExport from 'default-package'; | ||
import * as allExports from 'all-package'; | ||
` | ||
|
||
const expectedCode = ` | ||
import import1 from from 'test-package/import1'; | ||
import import2 from from 'test-package/import2'; | ||
import importNested from 'test-package/nested/importNested'; | ||
import defaultExport from 'default-package' | ||
import * as allExports from 'all-package' | ||
` | ||
|
||
it("should transform imports", async () => { | ||
const result = await transformImports(code); | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
import * as fs from 'node:fs'; | ||
import * as path from 'pathe'; | ||
import { describe, expect, it, vi } from 'vitest'; | ||
|
||
import { createContext } from '../../src/core/context'; | ||
import { generateImportStatement, generateImportTuple, transformImports, updateCode } from '../../src/core/transform'; | ||
import { getVersion } from '../../src/core/version'; | ||
import { ImportTuple } from '../../src/types'; | ||
|
||
vi.mock('../../src/core/version.ts'); | ||
|
||
describe('Transform', () => { | ||
const code = fs.readFileSync(path.join(process.cwd(), 'tests/fixtures/basic.ts'), 'utf8'); | ||
|
||
const expectedCode = `import map from 'lodash/map'; | ||
import LodashMerge from 'lodash/merge' | ||
import colors from 'picocolors' | ||
import UnderMap from 'underscore/map' | ||
const testMap = map([1, 2, 3], x => x + 1); | ||
const testMerge = LodashMerge({ a: 1 }, { b: 2 }); | ||
const testColors = colors.bold('test'); | ||
const testMap2 = Undermap([1, 2, 3], x => x + 1); | ||
export { testColors, testMap, testMap2, testMerge };`; | ||
|
||
describe('Generate import tuples', () => { | ||
it('maps one to one', () => { | ||
const tuple = generateImportTuple('map'); | ||
expect(tuple).toEqual(['map', 'map']); | ||
}); | ||
|
||
it('splits as statement', () => { | ||
const tuple = generateImportTuple('merge as LodashMerge'); | ||
expect(tuple).toEqual(['merge', 'LodashMerge']); | ||
}); | ||
}); | ||
|
||
const modules = [ | ||
{ module: 'lodash', transform: (moduleName: string, importName: string) => `${moduleName}/${importName}` }, | ||
]; | ||
const ctx = createContext({ modules, cwd: 'tests/fixtures' }); | ||
vi.mocked(getVersion).mockResolvedValue('2.0.0'); | ||
|
||
describe('Generating new import statements', () => { | ||
it('should generate import statements with one to one tuple', async () => { | ||
const tuple = ['map', 'map'] as ImportTuple; | ||
const imports = await generateImportStatement('lodash', tuple, ctx.modules.get('lodash')!, ctx); | ||
expect(imports).toEqual("import map from 'https://cdn.jsdelivr.net/npm/[email protected]/map/+esm';"); | ||
}); | ||
|
||
it('should generate import statements with renamed import', async () => { | ||
const tuple = ['merge', 'LodashMerge'] as ImportTuple; | ||
const imports = await generateImportStatement('lodash', tuple, ctx.modules.get('lodash')!, ctx); | ||
expect(imports).toEqual("import LodashMerge from 'https://cdn.jsdelivr.net/npm/[email protected]/merge/+esm';"); | ||
}); | ||
}); | ||
|
||
it.skip('updates code with new statements', () => { | ||
const newCode = updateCode(code, [['import import1 from \'https://cdn.jsdelivr.net/npm/test-package/[email protected]/+esm\';', 0, 59]]); | ||
|
||
expect(newCode).toEqual(expectedCode); | ||
}); | ||
|
||
|
||
it.skip('should transform imports', async () => { | ||
const result = await transformImports(code, ctx); | ||
expect(result).toEqual(expectedCode); | ||
}) | ||
}) | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,46 @@ | ||
import * as path from "pathe" | ||
import { rollup } from "rollup" | ||
import { describe, expect, it } from "vitest" | ||
import * as path from 'pathe'; | ||
import { rollup } from 'rollup'; | ||
import { describe, expect, it } from 'vitest'; | ||
|
||
import jsDelivr from '../src/rollup' | ||
import jsDelivr from '../src/rollup'; | ||
|
||
describe('Rollup build', () => { | ||
describe.skip("No transform", () => { | ||
describe.skip('Rollup build', () => { | ||
describe.skip('No transform', () => { | ||
it('should rewrite imports for basic', async () => { | ||
const bundle = await rollup({ | ||
input: "./tests/fixtures/basic.ts", | ||
input: './tests/fixtures/basic.ts', | ||
plugins: [jsDelivr({ cwd: path.join(process.cwd(), 'tests/fixtures'), modules: [{ module: 'lodash' }, { module: 'underscore' }] })] | ||
}) | ||
const { output } = await bundle.generate({ format: 'esm' }) | ||
expect(output[0].imports).toEqual(['https://cdn.jsdelivr.net/npm/[email protected]/+esm', 'picocolors', 'https://cdn.jsdelivr.net/npm/[email protected]/+esm']) | ||
}); | ||
const { output } = await bundle.generate({ format: 'esm' }); | ||
expect(output[0].imports).toEqual(['https://cdn.jsdelivr.net/npm/[email protected]/+esm', 'picocolors', 'https://cdn.jsdelivr.net/npm/[email protected]/+esm']); | ||
|
||
}) | ||
}); | ||
|
||
it('should skip rewrite imports for basic', async () => { | ||
const bundle = await rollup({ | ||
input: './tests/fixtures/basic.ts', | ||
plugins: [jsDelivr({ cwd: path.join(process.cwd(), 'tests/fixtures') })] | ||
}) | ||
const { output } = await bundle.generate({ format: 'esm' }) | ||
expect(output[0].imports).toEqual(['lodash', 'picocolors', 'underscore']) | ||
}) | ||
}) | ||
}); | ||
const { output } = await bundle.generate({ format: 'esm' }); | ||
expect(output[0].imports).toEqual(['lodash', 'picocolors', 'underscore']); | ||
}); | ||
}); | ||
|
||
describe("With transform", () => { | ||
describe('With transform', () => { | ||
it('should split imports and rewrite for basic', async () => { | ||
const bundle = await rollup({ | ||
input: "./tests/fixtures/basic.ts", | ||
input: './tests/fixtures/basic.ts', | ||
plugins: [jsDelivr({ | ||
cwd: path.join(process.cwd(), 'tests/fixtures'), modules: [{ | ||
module: 'lodash', transform: (moduleName, importName) => `${moduleName}/${importName}` | ||
}] | ||
})] | ||
}) | ||
const { output } = await bundle.generate({ format: 'esm' }) | ||
}); | ||
const { output } = await bundle.generate({ format: 'esm' }); | ||
console.log(output); | ||
// console.log(output) | ||
// expect(output[0].imports).toEqual(['https://cdn.jsdelivr.net/npm/[email protected]/+esm']) | ||
}) | ||
}) | ||
}); | ||
}); | ||
|
||
}) | ||
}); |