diff --git a/scripts/build/esm-exports.spec.ts b/scripts/build/esm-exports.spec.ts new file mode 100644 index 000000000..bbdc97973 --- /dev/null +++ b/scripts/build/esm-exports.spec.ts @@ -0,0 +1,33 @@ +import { rewriteInjectableExports } from './esm-exports'; + +describe('rewriteInjectableExports', () => { + test('rewrites inline class exports into instance exports', () => { + const jsFile = `import { AwesomeCordovaNativePlugin } from '@awesome-cordova-plugins/core'; +export class File extends AwesomeCordovaNativePlugin { + static pluginName = 'File'; +} +//# sourceMappingURL=index.js.map`; + + expect(rewriteInjectableExports(jsFile, [{ className: 'File', dirName: 'file', file: '/tmp/file/index.ts' }])) + .toBe(`import { AwesomeCordovaNativePlugin } from '@awesome-cordova-plugins/core'; +class FileOriginal extends AwesomeCordovaNativePlugin { + static pluginName = 'File'; +} +const File = new FileOriginal(); +export { File }; +//# sourceMappingURL=index.js.map`); + }); + + test('keeps supporting legacy named exports', () => { + const jsFile = `class SocialSharingOriginal {} +export { SocialSharingOriginal };`; + + expect( + rewriteInjectableExports(jsFile, [ + { className: 'SocialSharing', dirName: 'social-sharing', file: '/tmp/social-sharing/index.ts' }, + ]) + ).toBe(`class SocialSharingOriginal {} +const SocialSharing = new SocialSharingOriginal(); +export { SocialSharing };`); + }); +}); diff --git a/scripts/build/esm-exports.ts b/scripts/build/esm-exports.ts new file mode 100644 index 000000000..b2e80eee1 --- /dev/null +++ b/scripts/build/esm-exports.ts @@ -0,0 +1,45 @@ +import { InjectableClassEntry } from './transformers/extract-injectables'; + +const SOURCE_MAP_COMMENT = '\n//# sourceMappingURL='; + +function insertBeforeSourceMapComment(contents: string, addition: string): string { + const sourceMapIndex = contents.lastIndexOf(SOURCE_MAP_COMMENT); + if (sourceMapIndex === -1) { + return `${contents}\n${addition}`; + } + + return `${contents.slice(0, sourceMapIndex)}\n${addition}${contents.slice(sourceMapIndex)}`; +} + +export function rewriteInjectableExports(jsFile: string, classes: InjectableClassEntry[]): string { + let rewrittenFile = jsFile; + const exportBlocks: string[] = []; + + classes.forEach((entry) => { + const originalClassName = `${entry.className}Original`; + + rewrittenFile = rewrittenFile.replace( + new RegExp(`([\\s\\(])${entry.className}([\\s\\.;\\(,])`, 'g'), + `$1${originalClassName}$2` + ); + + const legacyExportPattern = new RegExp(`\\nexport\\s*\\{\\s*${originalClassName}\\s*\\};?`); + if (legacyExportPattern.test(rewrittenFile)) { + rewrittenFile = rewrittenFile.replace(legacyExportPattern, ''); + } else { + const inlineClassExportPattern = new RegExp(`export\\s+class\\s+${originalClassName}(?=\\s)`); + if (!inlineClassExportPattern.test(rewrittenFile)) { + throw new Error(`Could not rewrite ESM export for injectable class "${entry.className}"`); + } + rewrittenFile = rewrittenFile.replace(inlineClassExportPattern, `class ${originalClassName}`); + } + + exportBlocks.push(`const ${entry.className} = new ${originalClassName}();\nexport { ${entry.className} };`); + }); + + if (exportBlocks.length === 0) { + return rewrittenFile; + } + + return insertBeforeSourceMapComment(rewrittenFile, exportBlocks.join('\n')); +} diff --git a/scripts/tasks/build-esm.ts b/scripts/tasks/build-esm.ts index 22e69237c..feca599b0 100644 --- a/scripts/tasks/build-esm.ts +++ b/scripts/tasks/build-esm.ts @@ -1,6 +1,7 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; +import { rewriteInjectableExports } from '../build/esm-exports'; import { PLUGIN_PATHS, ROOT } from '../build/helpers'; import { InjectableClassEntry, EMIT_PATH } from '../build/transformers/extract-injectables'; import { generateDeclarations, transpile } from '../build/transpile'; @@ -17,17 +18,11 @@ outDirs.forEach((dir: string) => { let jsFile: string = readFileSync(join(dir, 'index.js'), 'utf-8'), dtsFile: string = readFileSync(join(dir, 'index.d.ts'), 'utf-8'); + jsFile = rewriteInjectableExports(jsFile, classes); + classes.forEach((entry) => { dtsFile = dtsFile.replace(`class ${entry.className} `, 'class ' + entry.className + 'Original '); dtsFile += `\nexport declare const ${entry.className}: ${entry.className}Original;`; - jsFile = jsFile.replace( - new RegExp(`([\\s\\(])${entry.className}([\\s\\.;\\(,])`, 'g'), - '$1' + entry.className + 'Original$2' - ); - jsFile = jsFile.replace( - `export { ${entry.className}Original }`, - `var ${entry.className} = new ${entry.className}Original();\nexport { ${entry.className} }` - ); }); writeFileSync(join(dir, 'index.js'), jsFile, 'utf-8');