Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .husky/pre-commit

This file was deleted.

4 changes: 0 additions & 4 deletions .husky/pre-push

This file was deleted.

36 changes: 34 additions & 2 deletions lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
24 changes: 22 additions & 2 deletions lib/utils/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')) {
Expand All @@ -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
Expand All @@ -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
}
Expand Down
18 changes: 18 additions & 0 deletions test/data/typescript-support/CustomHelper.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
return 'Async TypeScript helper method'
}
}

export default CustomHelper
15 changes: 15 additions & 0 deletions test/data/typescript-support/MaterialComponentHelper.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
console.log(`Clicking button: ${selector}`);
}
}

export default MaterialComponentHelper;
16 changes: 16 additions & 0 deletions test/data/typescript-support/abstract-helper.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
}
7 changes: 7 additions & 0 deletions test/data/typescript-support/abstract.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export abstract class AbstractHelper {
protected config: any;
constructor(config?: any) {
this.config = config;
}
abstract customMethod(): string;
}
9 changes: 9 additions & 0 deletions test/data/typescript-support/material.component.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AbstractHelper } from "./abstract.helper";

class MaterialComponentHelper extends AbstractHelper {
customMethod(): string {
return "Material component works";
}
}

export default MaterialComponentHelper;
53 changes: 53 additions & 0 deletions test/unit/container_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
})
Loading