diff --git a/packages/macros/src/babel/macros-babel-plugin.ts b/packages/macros/src/babel/macros-babel-plugin.ts index 2bdaead8c..e41ca0220 100644 --- a/packages/macros/src/babel/macros-babel-plugin.ts +++ b/packages/macros/src/babel/macros-babel-plugin.ts @@ -11,6 +11,8 @@ import error from './error'; import failBuild from './fail-build'; import { Evaluator, buildLiterals } from './evaluate-json'; import type * as Babel from '@babel/core'; +import { existsSync, readdirSync } from 'fs'; +import { resolve, dirname, join } from 'path'; export default function main(context: typeof Babel): unknown { let t = context.types; @@ -123,7 +125,52 @@ export default function main(context: typeof Babel): unknown { if (callee.referencesImport('@embroider/macros', 'importSync')) { let specifier = path.node.arguments[0]; if (specifier?.type !== 'StringLiteral') { - throw new Error(`importSync eager mode doesn't implement non string literal arguments yet`); + let relativePath = ''; + let property; + if (specifier.type === 'TemplateLiteral') { + relativePath = specifier.quasis[0].value.cooked!; + property = specifier.expressions[0] as t.Expression; + } + // babel might transform template form `../my-path/${id}` to '../my-path/'.concat(id) + if ( + specifier.type === 'CallExpression' && + specifier.callee.type === 'MemberExpression' && + specifier.callee.property.type === 'Identifier' && + specifier.callee.property.name === 'concat' && + specifier.callee.object.type === 'StringLiteral' + ) { + relativePath = specifier.callee.object.value; + property = specifier.arguments[0] as t.Expression; + } + if (property && relativePath && relativePath.startsWith('.')) { + const resolvedPath = resolve(dirname((state as any).filename), relativePath); + let entries: string[] = []; + if (existsSync(resolvedPath)) { + entries = readdirSync(resolvedPath).filter(e => !e.startsWith('.')); + } + const obj = t.objectExpression( + entries.map(e => { + let key = e.split('.')[0]; + const rest = e.split('.').slice(1, -1); + if (rest.length) { + key += `.${rest}`; + } + const id = t.callExpression( + state.importUtil.import(path, state.pathToOurAddon('es-compat2'), 'default', 'esc'), + [state.importUtil.import(path, join(relativePath, key).replace(/\\/g, '/'), '*')] + ); + return t.objectProperty(t.stringLiteral(key), id); + }) + ); + const memberExpr = t.memberExpression(obj, property, true); + path.replaceWith(memberExpr); + state.calledIdentifiers.add(callee.node); + return; + } else { + throw new Error( + `importSync eager mode only supports dynamic paths which are relative, must start with a '.', had ${specifier.type}` + ); + } } path.replaceWith( t.callExpression(state.importUtil.import(path, state.pathToOurAddon('es-compat2'), 'default', 'esc'), [ diff --git a/packages/macros/tests/babel/import-sync.test.ts b/packages/macros/tests/babel/import-sync.test.ts index 1d6b2529e..08c08ea32 100644 --- a/packages/macros/tests/babel/import-sync.test.ts +++ b/packages/macros/tests/babel/import-sync.test.ts @@ -53,5 +53,30 @@ describe('importSync', function () { `); expect(code).toMatch(/import \* as _importSync\d from "my-plugin"/); }); + test('importSync accepts template argument with dynamic part', () => { + let code = transform(` + import { importSync } from '@embroider/macros'; + function getFile(file) { + return importSync(\`../../\${file}\`).default; + } + `); + expect(code).toEqual(`import esc from "../../src/addon/es-compat2"; +import * as _importSync0 from "../../README"; +import * as _importSync20 from "../../jest.config"; +import * as _importSync30 from "../../node_modules"; +import * as _importSync40 from "../../package"; +import * as _importSync50 from "../../src"; +import * as _importSync60 from "../../tests"; +function getFile(file) { + return { + "README": esc(_importSync0), + "jest.config": esc(_importSync20), + "node_modules": esc(_importSync30), + "package": esc(_importSync40), + "src": esc(_importSync50), + "tests": esc(_importSync60) + }[file].default; +}`); + }); }); });