Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
25aa92b
feat: implement ESM support tests and transformation strategy
robertsLando Dec 4, 2025
98be81c
feat: complete Sprint 2 - ESM-aware module resolver
robertsLando Dec 4, 2025
2169840
fix: optimize package resolution conditions for CommonJS bundling
robertsLando Dec 4, 2025
00aca37
refactor: clean up code formatting and improve readability in various…
robertsLando Dec 4, 2025
b2ed8ba
Merge branch 'main' into esm-support
robertsLando Dec 11, 2025
0a37535
Merge branch 'main' into esm-support
robertsLando Feb 10, 2026
908b75b
chore: cleanup test artifacts and add type declarations for resolve.e…
robertsLando Feb 10, 2026
1df6258
fix: improve ESM resolution to avoid duplicate markers and validate p…
robertsLando Feb 10, 2026
43db4e5
feat: add test for aedes v1.0.0 ESM package and fix exports package d…
robertsLando Feb 10, 2026
bb242da
chore: update Copilot instructions for commit workflow and clean up c…
robertsLando Feb 10, 2026
90bfda2
feat: optimize ESM detection with caching and enhance follow function…
robertsLando Feb 10, 2026
dcec27f
feat: enhance package handling by adding synthetic main field and nor…
robertsLando Feb 10, 2026
00fd845
fix: improve ESM handling by updating conditions for exports resoluti…
robertsLando Feb 10, 2026
2e58f69
fix: update test filtering logic and enhance cleanup process for sign…
robertsLando Feb 10, 2026
f7c0159
feat: add additional npm tests for ESM compatibility and enhance outp…
robertsLando Feb 10, 2026
33a27bf
fix: update pnpm installation command to include --force option for c…
robertsLando Feb 10, 2026
971b572
fix: streamline module resolution by simplifying error handling and e…
robertsLando Feb 10, 2026
5247ed5
fix: enhance module resolution logic for better ESM and CJS compatibi…
robertsLando Feb 10, 2026
ef3b2e9
fix: simplify package.json resolution logic in tryResolveESM function
robertsLando Feb 10, 2026
d390f7c
fix: enhance error logging in detect function and include file context
robertsLando Feb 10, 2026
759d80b
fix: improve readability and structure in stepDetect function
robertsLando Feb 10, 2026
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
55 changes: 45 additions & 10 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This is a TypeScript-based Node.js project that packages Node.js applications in
- `dictionary/`: Package-specific configuration files for known npm packages
- `test/`: Comprehensive test suite with numbered test directories
- `examples/`: Example projects demonstrating pkg usage
- `plans/`: Implementation plans and design documents
- `.github/workflows/`: CI/CD configuration using GitHub Actions

## Development Workflow
Expand Down Expand Up @@ -49,6 +50,35 @@ npm run start
- Run `npm run fix` to automatically fix formatting and linting issues
- All changes must pass CI checks (linting, building, and tests)

#### Commit and Push Workflow

**CRITICAL: Follow this workflow for ALL commits and pushes:**

1. **Clean test artifacts**: Remove any test-generated output files (executables, binaries) before staging changes

- Test artifacts typically include: `*.exe`, `*-linux`, `*-macos`, `*-win.exe` in test directories
- Check staged files with `git status` and remove any test outputs
- These files may remain if a test failed before cleanup

2. **Verify no lint issues**:

- ALWAYS run `npm run lint` before committing
- Fix all linting errors with `npm run fix` or manually
- NEVER commit or push with lint errors present

3. **Request approval before commit/push**:

- Show the user what files will be committed (`git status --short`)
- Present a summary of changes made
- Wait for explicit user approval before running `git commit` and `git push`
- Do NOT commit or push without user confirmation

4. **Commit with conventional commits format**:
- Use: `feat:`, `fix:`, `refactor:`, `test:`, `chore:`, `docs:`
- Include detailed commit message explaining changes

This workflow ensures code quality, prevents accidental commits of test artifacts, and gives the user control over what gets committed to the repository.

#### Formatting

- Uses Prettier for code formatting
Expand Down Expand Up @@ -247,16 +277,21 @@ The project uses GitHub Actions workflows:

## Important Notes for Copilot Coding Agent

1. **Always build before testing**: Run `npm run build` before running any tests
2. **Use correct Node.js version**: The project requires Node.js >= 18.0.0
3. **Respect TypeScript compilation**: Edit `lib/*.ts` files, not `lib-es5/*.js` files
4. **Maintain test numbering**: When adding tests, choose appropriate test number (XX in test-XX-name)
5. **Check existing dictionary files**: Before adding new package support, review existing dictionary files for patterns
6. **Preserve backward compatibility**: This tool is widely used; breaking changes need careful consideration
7. **Cross-platform testing**: When possible, verify changes work on Linux, macOS, and Windows
8. **Native addon handling**: Be extra careful with changes affecting native addon loading and extraction
9. **Snapshot filesystem**: Changes to virtual filesystem handling require thorough testing
10. **Performance matters**: Packaging time and executable size are important metrics
1. **NEVER commit without user approval**: Always show changes with `git status --short`, present a summary, and wait for explicit user confirmation before running `git commit` or `git push`
2. **ALWAYS check lint before committing**: Run `npm run lint` before every commit and fix all issues - NEVER commit with lint errors
3. **Clean test artifacts before staging**: Remove any test-generated executables (`*.exe`, `*-linux`, `*-macos`, `*-win.exe`) from test directories before committing
4. **Always build before testing**: Run `npm run build` before running any tests
5. **Use correct Node.js version**: The project requires Node.js >= 18.0.0
6. **Use Yarn for package management**: This project uses `yarn`, not `npm`, for dependency management
7. **Respect TypeScript compilation**: Edit `lib/*.ts` files, not `lib-es5/*.js` files
8. **Maintain test numbering**: When adding tests, choose appropriate test number (XX in test-XX-name)
9. **Check existing dictionary files**: Before adding new package support, review existing dictionary files for patterns
10. **Preserve backward compatibility**: This tool is widely used; breaking changes need careful consideration
11. **Cross-platform testing**: When possible, verify changes work on Linux, macOS, and Windows
12. **Native addon handling**: Be extra careful with changes affecting native addon loading and extraction
13. **Snapshot filesystem**: Changes to virtual filesystem handling require thorough testing
14. **Performance matters**: Packaging time and executable size are important metrics
15. **Implementation plans**: Store all implementation plans and design documents in the `plans/` directory

## Git Workflow

Expand Down
90 changes: 90 additions & 0 deletions lib/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,93 @@ export function toNormalizedRealPath(requestPath: string) {

return file;
}

/**
* Find the nearest package.json file by walking up the directory tree
* @param filePath - Starting file path
* @returns Path to package.json or null if not found
*/
function findNearestPackageJson(filePath: string): string | null {
let dir = path.dirname(filePath);
const { root } = path.parse(dir);

while (dir !== root) {
const packageJsonPath = path.join(dir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
return packageJsonPath;
}
dir = path.dirname(dir);
}

return null;
}

// Caches for ESM detection performance optimization
const packageJsonCache = new Map<string, string | null>();
const esmPackageCache = new Map<string, boolean>();

/**
* Check if a package.json indicates an ESM package
* @param packageJsonPath - Path to package.json
* @returns true if "type": "module" is set
*/
export function isESMPackage(packageJsonPath: string): boolean {
// Check cache first
if (esmPackageCache.has(packageJsonPath)) {
return esmPackageCache.get(packageJsonPath)!;
}

try {
const content = fs.readFileSync(packageJsonPath, 'utf8');
const pkg = JSON.parse(content);
const result = pkg.type === 'module';
esmPackageCache.set(packageJsonPath, result);
return result;
} catch {
esmPackageCache.set(packageJsonPath, false);
return false;
}
}

/**
* Determine if a file should be treated as ESM
* Based on file extension and nearest package.json "type" field
*
* @param filePath - The file path to check
* @returns true if file should be treated as ESM
*/
export function isESMFile(filePath: string): boolean {
// .mjs files are always ESM
if (filePath.endsWith('.mjs')) {
return true;
}

// .cjs files are never ESM
if (filePath.endsWith('.cjs')) {
return false;
}

// For .js files, check nearest package.json for "type": "module"
if (filePath.endsWith('.js')) {
const dir = path.dirname(filePath);

// Check cache first
if (packageJsonCache.has(dir)) {
const cached = packageJsonCache.get(dir);
if (cached) {
return isESMPackage(cached);
}
return false;
}

// Compute and cache
const packageJsonPath = findNearestPackageJson(filePath);
packageJsonCache.set(dir, packageJsonPath);

if (packageJsonPath) {
return isESMPackage(packageJsonPath);
}
}

return false;
}
5 changes: 3 additions & 2 deletions lib/detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,13 +502,14 @@ export function parse(body: string) {
});
}

export function detect(body: string, visitor: VisitorFunction) {
export function detect(body: string, visitor: VisitorFunction, file?: string) {
let json;

try {
json = parse(body);
} catch (error) {
log.warn(`Babel parse has failed: ${(error as Error).message}`);
const fileInfo = file ? ` in ${file}` : '';
log.warn(`Babel parse has failed: ${(error as Error).message}${fileInfo}`);
}

if (!json) {
Expand Down
66 changes: 66 additions & 0 deletions lib/esm-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as babel from '@babel/core';
import { log } from './log';

export interface TransformResult {
code: string;
isTransformed: boolean;
}

/**
* Transform ESM code to CommonJS using Babel
* This allows ESM modules to be compiled to bytecode via vm.Script
*
* @param code - The ESM source code to transform
* @param filename - The filename for error reporting
* @returns Object with transformed code and success flag
*/
export function transformESMtoCJS(
code: string,
filename: string,
): TransformResult {
try {
const result = babel.transformSync(code, {
filename,
plugins: [
[
'@babel/plugin-transform-modules-commonjs',
{
strictMode: true,
allowTopLevelThis: true,
},
],
],
sourceMaps: false,
compact: false,
// Don't modify other syntax, only transform import/export
presets: [],
// Prevent Babel from loading user config files
babelrc: false,
configFile: false,
sourceType: 'module',
});
Comment on lines +22 to +41
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Babel module transform here only addresses import/export. ESM features that don’t have a safe CJS equivalent (notably top-level await and import.meta) can survive the transform and then become invalid when executed via the CommonJS wrapper / vm.Script, leading to runtime syntax errors. It’d be safer to explicitly detect these constructs and either fail fast with a clear error, or fall back to a supported execution mode (e.g. skip bytecode and keep ESM without rewriting package.json type).

Copilot uses AI. Check for mistakes.

if (!result || !result.code) {
log.warn(`Babel transform returned no code for ${filename}`);
return {
code,
isTransformed: false,
};
}

return {
code: result.code,
isTransformed: true,
};
} catch (error) {
log.warn(
`Failed to transform ESM to CJS for ${filename}: ${
error instanceof Error ? error.message : String(error)
}`,
);
return {
code,
isTransformed: false,
};
}
}
Loading