Skip to content

fix(@angular/cli): resolve schematic working directory from project root - #34608

Closed
ant1quar wants to merge 1 commit into
angular:mainfrom
ant1quar:fix/21660-generate-default-path
Closed

ant1quar wants to merge 1 commit into
angular:mainfrom
ant1quar:fix/21660-generate-default-path

Conversation

@ant1quar

Copy link
Copy Markdown

PR Checklist

PR Type

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #21660

In a multi-project workspace, ng generate from a project root (e.g. projects/app1) uses the workspace-relative working directory as the schematic path default. Files are created under the project folder instead of src/app / src/lib.

What is the new behavior?

The workingDirectory smart default is resolved relative to the active project root. From the project root, schematics receive no working-directory override and apply their usual default path (src/app or src/lib). From deeper folders inside the project, paths remain relative to the project tree.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Fixes #21660

Tests:

  • pnpm bazel test //packages/angular/cli:test --test_filter="getSchematicWorkingDirectory"

Made with Cursor

Compute the workingDirectory smart default relative to the active project
root so ng generate from a multi-project application directory uses src/app
defaults instead of the project folder itself.

Fixes angular#21660

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new utility function, getSchematicWorkingDirectory, to resolve the workingDirectory smart default for schematics relative to the project root. The reviewer identified potential issues where the resolved path could escape the project or workspace root and provided code suggestions to guard against these cases by returning undefined. Additionally, the reviewer recommended adding a unit test to verify the behavior when the current working directory is outside the project root.

Comment on lines +344 to +346
const projectRelative = devkitNormalize(path.relative(projectRoot, cwd));

return projectRelative === '' ? undefined : projectRelative;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In a single-project workspace where the project root is not the workspace root (e.g., projects/app), if the command is run from the workspace root, workspace.projects.size === 1 will eagerly select the single project. This causes path.relative(projectRoot, cwd) to resolve to a path escaping the project root (e.g., ../..). Returning this relative path will cause schematics to generate files outside the project root (at the workspace root).

To prevent this, we should check if the resolved relative path starts with .. or is absolute, and return undefined in those cases so that schematics fall back to their default paths.

      const projectRelative = devkitNormalize(path.relative(projectRoot, cwd));
      if (projectRelative.startsWith('..') || path.isAbsolute(projectRelative)) {
        return undefined;
      }

      return projectRelative === '' ? undefined : projectRelative;

Comment on lines +50 to +56
it('returns undefined for a single-project workspace when cwd is the workspace root', () => {
const workspace = mockWorkspace({
app: { root: '' },
});

expect(getSchematicWorkingDirectory(workspace, workspaceRoot)).toBeUndefined();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Add a unit test to verify that getSchematicWorkingDirectory returns undefined when the single project has a non-empty root and the command is run from the workspace root (outside the project root).

  it('returns undefined for a single-project workspace when cwd is the workspace root', () => {
    const workspace = mockWorkspace({
      app: { root: '' },
    });

    expect(getSchematicWorkingDirectory(workspace, workspaceRoot)).toBeUndefined();
  });

  it('returns undefined for a single-project workspace when cwd is outside the project root', () => {
    const workspace = mockWorkspace({
      app: { root: 'projects/app' },
    });

    expect(getSchematicWorkingDirectory(workspace, workspaceRoot)).toBeUndefined();
  });

Comment on lines +350 to +352
const workspaceRelative = devkitNormalize(path.relative(workspace.basePath, cwd));

return workspaceRelative === '' ? undefined : workspaceRelative;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Similarly, if cwd is outside the workspace base path, workspaceRelative could start with .. or be absolute. We should guard against this to ensure we only return paths within the workspace.

  const workspaceRelative = devkitNormalize(path.relative(workspace.basePath, cwd));
  if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)) {
    return undefined;
  }

  return workspaceRelative === '' ? undefined : workspaceRelative;

@alan-agius4

Copy link
Copy Markdown
Collaborator

Thank you for the pull request.

There are two issues with this change:

  1. The schematic Tree is rooted at the workspace root, so options.path must be relative to the workspace root (matching buildDefaultPath, which returns /${project.sourceRoot}/app or /${project.root}/src/lib). Resolving workingDirectory relative to project.root strips the project directory prefix, which causes running ng generate inside a subdirectory like projects/lib/feature to generate files at /feature in the workspace root instead of /projects/lib/feature.
  2. More broadly, there are currently two distinct behaviors: running from the workspace root falls back to buildDefaultPath, whereas running inside a directory generates directly in that directory. Changing how workingDirectory and buildDefaultPath behave when running inside project directories (for example, in libraries with secondary entry points or custom sourceRoot layouts) has breaking change implications and requires a larger design discussion first.

Closing this pull request for now, as any change here will need a broader design review.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI generates items in unexpected workspace directories

2 participants