Skip to content

fix: support ESM templates in templates:install (require → dynamic import) - #83

Open
AnujVishwakarma-src wants to merge 2 commits into
adobe:mainfrom
AnujVishwakarma-src:APPBLD-4503-esm-template-support
Open

AnujVishwakarma-src wants to merge 2 commits into
adobe:mainfrom
AnujVishwakarma-src:APPBLD-4503-esm-template-support

Conversation

@AnujVishwakarma-src

Copy link
Copy Markdown
Contributor

Problem

aio app add extensiontemplates:install runs the selected template with:

const gen = await env.instantiate(require(templatePath), { ... })

When a template ships as ESM (type: module), require(templatePath):

  • throws ERR_REQUIRE_ESM on Node without require(esm), or
  • on newer Node (20.19+/22.12+/24+) returns the module namespace { __esModule, default: <class> } — not the generator class.

Either way env.instantiate() gets a non-constructor and throws Error: constructor is not a constructor. This is hit today by @adobe/generator-app-excshell and @adobe/generator-app-asset-compute (both now ESM), since the Template Registry installs templates at @latest, bypassing semver pins.

Fix

Load the template resiliently and unwrap the default export (works for both CommonJS and ESM templates):

let templateModule
try {
  templateModule = require(templatePath)
} catch (e) {
  if (e.code === 'ERR_REQUIRE_ESM') {
    templateModule = await import(pathToFileURL(templatePath).href)
  } else {
    throw e
  }
}
const TemplateGenerator = (templateModule && templateModule.default) || templateModule
const gen = await env.instantiate(TemplateGenerator, { ... })

Tests

  • New: ESM template (require throws ERR_REQUIRE_ESM → dynamic import(), real ESM fixture) instantiates the default-export class.
  • New: a non-ERR_REQUIRE_ESM require error propagates.
  • Existing CommonJS-template tests unchanged.
  • Full suite: 153 tests, 100% coverage, lint clean.

🤖 Generated with Claude Code

templates:install loaded the template generator via require(templatePath) and
passed the result straight to env.instantiate(). For an ESM template (type:module)
require() either throws ERR_REQUIRE_ESM (Node without require(esm)) or, on newer
Node, returns the module namespace object rather than the generator class — so
env.instantiate() received a non-constructor and threw 'constructor is not a
constructor' (e.g. running @adobe/generator-app-excshell once it shipped as ESM).

Load the template resiliently: require() a CommonJS template, fall back to dynamic
import() on ERR_REQUIRE_ESM, and unwrap a .default export in both cases. Works for
CommonJS and ESM templates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AnujVishwakarma-src

Copy link
Copy Markdown
Contributor Author

/upload-coverage

@github-actions

Copy link
Copy Markdown

Coverage artifact not found. Re-run CI first, then comment /upload-coverage again.

@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

AnujVishwakarma-src added a commit to adobe/generator-app-asset-compute that referenced this pull request Sep 16, 2026
…on (#84)

The ESM 4.x releases broke 'aio app add extension' on the released aio-cli:
the Template Registry installs this template at @latest and app-templates
loads it with require(), which cannot instantiate an ESM generator
('constructor is not a constructor'). Revert the source to CommonJS (the
3.0.0 tree) and publish as 5.0.0 so @latest is CJS again.

Temporary until aio-cli-plugin-app-templates loads templates via import()
(adobe/aio-cli-plugin-app-templates#83) ships in a new aio-cli; this package
will then be re-migrated to ESM. generator-aio-app@10 keeps the ESM 4.1.0
via its ^4 range (unaffected by this major).

Co-authored-by: Anuj Vishwakarma <298213938+AnujVishwakarma-src@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AnujVishwakarma-src added a commit to adobe/generator-app-excshell that referenced this pull request Sep 16, 2026
…on (#68)

The ESM 3.x releases broke 'aio app add extension' on the released aio-cli:
the Template Registry installs this template at @latest and app-templates
loads it with require(), which cannot instantiate an ESM generator
('constructor is not a constructor'). Revert the source to CommonJS (the
2.0.0 tree) and publish as 4.0.0 so @latest is CJS again.

Temporary until aio-cli-plugin-app-templates loads templates via import()
(adobe/aio-cli-plugin-app-templates#83) ships in a new aio-cli; this package
will then be re-migrated to ESM. generator-aio-app@10 keeps the ESM 3.1.0
via its ^3 range (unaffected by this major).

Co-authored-by: Anuj Vishwakarma <298213938+AnujVishwakarma-src@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AnujVishwakarma-src

Copy link
Copy Markdown
Contributor Author

Findings

P1 / P2: ERR_REQUIRE_ASYNC_MODULE is not handled

In src/commands/templates/install.js, the fallback only catches:

if (e.code === 'ERR_REQUIRE_ESM') {
templateModule = await import(pathToFileURL(templatePath).href)
}

On newer Node versions, require() can load synchronous ESM directly. However, if the ESM template or anything in its import graph uses top-level await, Node throws ERR_REQUIRE_ASYNC_MODULE, not ERR_REQUIRE_ESM. Node explicitly documents that such modules must be loaded with import().

That means an otherwise valid ESM Yeoman template can still fail in the exact code path this PR is intended to fix.

Suggested change:

} catch (e) {
if (e.code === 'ERR_REQUIRE_ESM' || e.code === 'ERR_REQUIRE_ASYNC_MODULE') {
templateModule = await import(pathToFileURL(templatePath).href)
} else {
throw e
}
}

I would treat this as the main review comment.

Addresses review: on newer Node, require() loads sync ESM directly, but an
ESM template (or anything in its import graph) using top-level await throws
ERR_REQUIRE_ASYNC_MODULE (not ERR_REQUIRE_ESM). Handle both codes so such a
template still loads via dynamic import().

Adds a fixture with top-level await + a test covering the async-ESM path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AnujVishwakarma-src

Copy link
Copy Markdown
Contributor Author

Good catch — addressed in 94a520b.

require() now falls back to dynamic import() on both ERR_REQUIRE_ESM and ERR_REQUIRE_ASYNC_MODULE:

if (e.code === 'ERR_REQUIRE_ESM' || e.code === 'ERR_REQUIRE_ASYNC_MODULE') {
  templateModule = await import(pathToFileURL(templatePath).href)
} else {
  throw e
}

So an ESM template that uses top-level await (which throws ERR_REQUIRE_ASYNC_MODULE under require(esm) on newer Node) still loads correctly.

Added test/__fixtures__/esm-template-async/index.mjs (a fixture with top-level await) and a test covering the async-ESM path. Full suite: 154 tests, 100% coverage, lint clean.

@AnujVishwakarma-src

Copy link
Copy Markdown
Contributor Author

/upload-coverage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant