diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 3f4f6f4eb..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -prettier $(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') --write --ignore-unknown -git update-index --again -npx eslint $(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') -npm run dtslint diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index 3e18ca2e4..000000000 --- a/.husky/pre-push +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npm run test:unit diff --git a/lib/container.js b/lib/container.js index e1f68008a..e5bdb4dfb 100644 --- a/lib/container.js +++ b/lib/container.js @@ -398,20 +398,52 @@ async function requireHelperFromModule(helperName, config, HelperClass) { throw err } } else { + // Handle TypeScript files + let importPath = moduleName + let tempJsFile = null + const ext = path.extname(moduleName) + + if (ext === '.ts') { + try { + // Use the TypeScript transpilation utility + const typescript = await import('typescript') + const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript) + + debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`) + + importPath = tempFile + tempJsFile = allTempFiles + } catch (tsError) { + throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`) + } + } + // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName. try { // Try dynamic import for both CommonJS and ESM modules - const mod = await import(moduleName) + const mod = await import(importPath) if (!mod && !mod.default) { throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`) } HelperClass = mod.default || mod + + // Clean up temp files if created + if (tempJsFile) { + const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile] + cleanupTempFiles(filesToClean) + } } catch (err) { + // Clean up temp files before rethrowing + if (tempJsFile) { + const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile] + cleanupTempFiles(filesToClean) + } + if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) { // This is an ESM module, use dynamic import try { const pathModule = await import('path') - const absolutePath = pathModule.default.resolve(moduleName) + const absolutePath = pathModule.default.resolve(importPath) const mod = await import(absolutePath) HelperClass = mod.default || mod debug(`helper ${helperName} loaded via ESM import`) diff --git a/lib/utils/typescript.js b/lib/utils/typescript.js index 7cd417f86..c19badfc3 100644 --- a/lib/utils/typescript.js +++ b/lib/utils/typescript.js @@ -142,8 +142,13 @@ const __dirname = __dirname_fn(__filename); } } - // Try adding .ts extension if file doesn't exist and no extension provided - if (!path.extname(importedPath)) { + // Check for standard module extensions to determine if we should try adding .ts + const ext = path.extname(importedPath) + const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node'] + const hasStandardExtension = standardExtensions.includes(ext.toLowerCase()) + + // If it doesn't end with .ts and doesn't have a standard extension, try adding .ts + if (!importedPath.endsWith('.ts') && !hasStandardExtension) { const tsPath = importedPath + '.ts' if (fs.existsSync(tsPath)) { importedPath = tsPath @@ -168,6 +173,7 @@ const __dirname = __dirname_fn(__filename); /from\s+['"](\..+?)(?:\.ts)?['"]/g, (match, importPath) => { let resolvedPath = path.resolve(fileBaseDir, importPath) + const originalExt = path.extname(importPath) // Handle .js extension that might be .ts if (resolvedPath.endsWith('.js')) { @@ -181,6 +187,8 @@ const __dirname = __dirname_fn(__filename); } return `from '${relPath}'` } + // Keep .js extension as-is (might be a real .js file) + return match } // Try with .ts extension @@ -197,6 +205,18 @@ const __dirname = __dirname_fn(__filename); return `from '${relPath}'` } + // If the import doesn't have a standard module extension (.js, .mjs, .cjs, .json) + // add .js for ESM compatibility + // This handles cases where: + // 1. Import has no real extension (e.g., "./utils" or "./helper") + // 2. Import has a non-standard extension that's part of the name (e.g., "./abstract.helper") + const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node'] + const hasStandardExtension = standardExtensions.includes(originalExt.toLowerCase()) + + if (!hasStandardExtension) { + return match.replace(importPath, importPath + '.js') + } + // Otherwise, keep the import as-is return match } diff --git a/test/data/typescript-support/CustomHelper.ts b/test/data/typescript-support/CustomHelper.ts new file mode 100644 index 000000000..7429ff8c6 --- /dev/null +++ b/test/data/typescript-support/CustomHelper.ts @@ -0,0 +1,18 @@ +// TypeScript custom helper for testing +class CustomHelper { + constructor(config: any) { + this.config = config + } + + config: any + + customMethod(): string { + return 'TypeScript helper loaded successfully' + } + + async asyncCustomMethod(): Promise { + return 'Async TypeScript helper method' + } +} + +export default CustomHelper diff --git a/test/data/typescript-support/MaterialComponentHelper.ts b/test/data/typescript-support/MaterialComponentHelper.ts new file mode 100644 index 000000000..f9aac214d --- /dev/null +++ b/test/data/typescript-support/MaterialComponentHelper.ts @@ -0,0 +1,15 @@ +// TypeScript custom helper that imports another TypeScript file +// Testing import without extension (should work after fix) +import { AbstractHelper, HelperUtils } from "./abstract-helper"; + +class MaterialComponentHelper extends AbstractHelper { + customMethod(): string { + return HelperUtils.formatMessage('Material component helper loaded'); + } + + async clickButton(selector: string): Promise { + console.log(`Clicking button: ${selector}`); + } +} + +export default MaterialComponentHelper; diff --git a/test/data/typescript-support/abstract-helper.ts b/test/data/typescript-support/abstract-helper.ts new file mode 100644 index 000000000..12d43054d --- /dev/null +++ b/test/data/typescript-support/abstract-helper.ts @@ -0,0 +1,16 @@ +// Abstract helper base class for testing imports +export abstract class AbstractHelper { + protected config: any; + + constructor(config?: any) { + this.config = config; + } + + abstract customMethod(): string; +} + +export class HelperUtils { + static formatMessage(msg: string): string { + return `[Helper] ${msg}`; + } +} diff --git a/test/data/typescript-support/abstract.helper.ts b/test/data/typescript-support/abstract.helper.ts new file mode 100644 index 000000000..10c4e8933 --- /dev/null +++ b/test/data/typescript-support/abstract.helper.ts @@ -0,0 +1,7 @@ +export abstract class AbstractHelper { + protected config: any; + constructor(config?: any) { + this.config = config; + } + abstract customMethod(): string; +} diff --git a/test/data/typescript-support/material.component.helper.ts b/test/data/typescript-support/material.component.helper.ts new file mode 100644 index 000000000..a3f252e06 --- /dev/null +++ b/test/data/typescript-support/material.component.helper.ts @@ -0,0 +1,9 @@ +import { AbstractHelper } from "./abstract.helper"; + +class MaterialComponentHelper extends AbstractHelper { + customMethod(): string { + return "Material component works"; + } +} + +export default MaterialComponentHelper; diff --git a/test/unit/container_test.js b/test/unit/container_test.js index ccc93be91..1a3dd0a89 100644 --- a/test/unit/container_test.js +++ b/test/unit/container_test.js @@ -350,5 +350,58 @@ describe('Container', () => { expect(I.loadModule).to.be.a('function') // The test verifies that the file loads without "ReferenceError: require is not defined" }) + + it('should load TypeScript custom helper', async () => { + const tsHelperPath = path.join(__dirname, '../data/typescript-support/CustomHelper.ts') + await container.create({ + helpers: { + CustomHelper: { + require: tsHelperPath, + }, + }, + }) + await container.started() + + const helper = container.helpers('CustomHelper') + expect(helper).to.be.ok + expect(helper.customMethod).to.be.a('function') + expect(helper.customMethod()).to.eql('TypeScript helper loaded successfully') + expect(helper.asyncCustomMethod).to.be.a('function') + }) + + it('should load TypeScript helper that imports another TypeScript file without extension', async () => { + const tsHelperPath = path.join(__dirname, '../data/typescript-support/MaterialComponentHelper.ts') + await container.create({ + helpers: { + MaterialComponentHelper: { + require: tsHelperPath, + }, + }, + }) + await container.started() + + const helper = container.helpers('MaterialComponentHelper') + expect(helper).to.be.ok + expect(helper.customMethod).to.be.a('function') + expect(helper.customMethod()).to.eql('[Helper] Material component helper loaded') + expect(helper.clickButton).to.be.a('function') + }) + + it('should load TypeScript helper with dots in filename that imports another file with dots', async () => { + const tsHelperPath = path.join(__dirname, '../data/typescript-support/material.component.helper.ts') + await container.create({ + helpers: { + MaterialComponentHelper: { + require: tsHelperPath, + }, + }, + }) + await container.started() + + const helper = container.helpers('MaterialComponentHelper') + expect(helper).to.be.ok + expect(helper.customMethod).to.be.a('function') + expect(helper.customMethod()).to.eql('Material component works') + }) }) })