Skip to content

Enhance Ember project detection#70

Open
evoactivity wants to merge 1 commit intomasterfrom
fix/pnpm-monorepo-detection
Open

Enhance Ember project detection#70
evoactivity wants to merge 1 commit intomasterfrom
fix/pnpm-monorepo-detection

Conversation

@evoactivity
Copy link
Copy Markdown
Member

@evoactivity evoactivity commented Apr 18, 2026

This adds more aggressive ember project detection methods.

I often open a pnpm monorepo package as my workspace which means workspace.findFiles will not match files because the node_modules symlinks point outside the workspace root.

This uses 5 methods to detect an ember project starting with the cheapest

  1. Force enable
  2. Look for ember-cli-build.* file
  3. Read dependencies of workspace root package.json
  4. Use node resolver to find files
  5. Manual walk through node_modules

Tested on my project and it works.

@evoactivity evoactivity requested a review from lifeart April 18, 2026 10:43
Comment thread src/workspace-utils.ts
@@ -1,21 +1,226 @@
import { workspace } from 'vscode';
import * as path from 'path';
import { createRequire } from 'module';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure we could have it for web-worker version, wondering if we could do lazy import of createRequire?

Comment thread src/workspace-utils.ts

async function checkBuildFile(folderFsPath: string): Promise<boolean> {
for (const name of EMBER_CLI_BUILD_FILES) {
const uri = Uri.file(path.join(folderFsPath, name));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure we could use path.join (for example, for virtual modules)
https://github.com/microsoft/vscode-uri#usage-util wondering if Utils.joinPath or Utils.resolvePath(URI, paths) help us here

Comment thread src/workspace-utils.ts
return false;
}

async function isMonorepoRoot(dir: string): Promise<boolean> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let's give it a more explicit name: isPNPMMonorepoRoot (to not confuse ourselfs)

Copy link
Copy Markdown
Contributor

@lifeart lifeart left a comment

Choose a reason for hiding this comment

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

A few additional observations beyond the earlier thread. Nothing blocking; mainly correctness/UX polish.

One meta point: this extension declares "browser": "./dist/web/client/browserClientMain" in package.json, so web-worker compatibility (raised earlier on createRequire) is a live concern, not hypothetical.

Also — no tests were added. The tiered detection has several non-obvious branches (resolver path, walk cap, monorepo-root detection, the step-counter logic below); a small table-driven test over a tmp fixture tree would make this much safer to evolve.

Comment thread src/workspace-utils.ts
// the top of the workspace; going one level above handles layouts that
// nest the repo inside another tooling directory.
if (await isMonorepoRoot(dir)) {
stepsAfterMonorepoRoot = 1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Off-by-one vs the comment on line 194. The comment says "allow one more ancestor after we hit one," but setting stepsAfterMonorepoRoot = 1 actually causes markers to be checked at the monorepo root and two ancestors above it:

  • iter N (monorepo root): check markers, set steps = 1, go to parent
  • iter N+1: check markers, steps === 1 → decrement to 0, go to parent
  • iter N+2: check markers, steps === 0 → break

So we visit three directories starting at the monorepo root, not two. Either set stepsAfterMonorepoRoot = 0 here, or update the comment to match ("allow two more ancestors"). Worth a test either way.

Comment thread src/workspace-utils.ts
const folders: ReadonlyArray<WorkspaceFolder> =
workspace.workspaceFolders ?? [];
if (folders.length === 0) {
return false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When detection returns false across all folders, the user gets no signal — they just see the extension silently not activate, and forceEnable is the only recourse. Consider an output-channel log line ("ELS: no Ember markers found in ; set els.detection.forceEnable to override"). Saves a round-trip when someone hits a new exotic layout.

Comment thread src/workspace-utils.ts
if (!deps || typeof deps !== 'object') continue;
const depNames = Object.keys(deps as Record<string, unknown>);
for (const marker of EMBER_MARKER_PACKAGES) {
if (depNames.indexOf(marker) !== -1) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: marker in deps (or Object.prototype.hasOwnProperty.call(deps, marker)) would skip the Object.keys allocation and the O(N) scan per marker — looks up directly in the dep map. depNames.includes(marker) is also clearer than .indexOf(...) !== -1 if you prefer to keep the keys array.

Comment thread package.json
"workspaceContains:ember-cli-build.js",
"workspaceContains:**/ember-cli-build.js",
"workspaceContains:**/ember-cli-build.cjs",
"workspaceContains:**/node_modules/ember-source/package.json",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two concerns on the activation globs here:

  1. Broader match than before. workspaceContains:**/ember-cli-build.js matches nested copies too (e.g. vendored build files inside unrelated packages), which will activate ELS in non-Ember roots and can slow activation on large monorepos. Previously this was root-only (workspaceContains:ember-cli-build.js). Consider keeping the root-only pattern and letting the richer detection logic handle deeper cases.
  2. Marker drift. EMBER_MARKER_PACKAGES in workspace-utils.ts lists six packages (incl. glimmer-lite-core, @glimmerx/core, ember-template-imports, etc.), but activation only triggers on ember-source. If a user opens a repo that only depends on one of the other markers and the extension never activates for a onLanguage:* reason, detection never runs. Either expand the workspaceContains list to match, or document that non-ember-source projects rely on a language activation.

Comment thread package.json
"devDependencies": {
"@types/mocha": "^2.2.33",
"@types/node": "^6.0.52",
"@types/node": "^14.18.63",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unrelated to the detection feature — an eight-major-version bump of @types/node (6 → 14) is likely to surface type-surface changes elsewhere. Worth splitting into its own PR, or at minimum confirming tsc --noEmit is clean across the repo (and the browser/web build still type-checks, since that bundle swaps Node builtins for shims).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The big bump was needed to properly type createRequire, technically createRequire needs 12.x.x but 14.x.x matches the major version of node shipped in vscode 1.60.0, which is the minimum engine version defined in package.json.

I've built with tsc but I'll do a full check for browser/web build.

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.

2 participants