Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): support path wildcards in enforce-module-boundaries autofix #18316

Merged

Conversation

dearlordylord
Copy link
Contributor

…ofix

What it solves: supports wildcards from https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping in enforce-module-boundaries --fix

Original discussion and bug report https://nrwlcommunity.slack.com/archives/CMFKWPU6Q/p1690292877751699

Current Behavior

only basic case seems to be handled, the one that is generated by default generators

Expected Behavior

More advanced aliasing https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping, including paths: {"@currents/graph-internals/*": ["libs/graph-internals/src/lib/*"],} , is desirable

Related Issue(s)

None

@dearlordylord dearlordylord requested a review from a team as a code owner July 26, 2023 09:44
@vercel
Copy link

vercel bot commented Jul 26, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
nx-dev ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 28, 2023 7:00am

@meeroslav meeroslav self-assigned this Jul 26, 2023
@@ -358,14 +358,20 @@ export default createESLintRule<Options, MessageIds>({
dirname(fileName),
dirname(importPath)
);
const maybeTagRelative = (path: string): string =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this needed? Wasn't relative(...) above already returning a relative path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's relative but when there's only one level of relativity (./), relative() would cut ./ making i.e. expected ./my/module into my/module: it goes into imports like this then import { some } from 'my/module' which makes resolver think it's an alias or a node lib, whereas what we really need is import { some } from './my/module' to reflect that we import relatively to the same dir.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wow, that's unexpected behavior of that function. Thanks for the clarification

Copy link
Contributor

@meeroslav meeroslav Jul 27, 2023

Choose a reason for hiding this comment

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

Can we name this something like ensureRelativeForm or something along those lines. This name doesn't really explain what the function does.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ensureRelativeForm sounds good, I didn't come up with a better name. Renamed!

@@ -81,6 +115,29 @@ function hasMemberExport(exportedMember, filePath) {
}

export function getRelativeImportPath(exportedMember, filePath, basePath) {
const TSX_SUFFIX = '.tsx';
Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer to have these explicitly used. It makes code shorter and more readable as it doesn't encapsulate the extension.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it. I'll change it back soon. Although I think that encapsulating extension doesn't harm readability, I respect the project code guidelines.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Inlined

return getOneOrNone(
Object.entries(paths)
.map(([src, dests]) => {
const isWildcard = src.endsWith(WILDCARD_TS_SPECIAL_CASE);
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, please use * directly. It makes the code more readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Inlined

@meeroslav meeroslav changed the title feat(linter): support path wildcards in enforce-module-boundaries aut… feat(linter): support path wildcards in enforce-module-boundaries autofix Jul 27, 2023
@@ -27,9 +27,43 @@ function tryReadBaseJson() {
*/
export function getBarrelEntryPointByImportScope(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this simpler?

The getOneOrNone seems to be doing a bit more than that since it also returns the result of the function. Additionally, if we are using generics, then they should extend from something other than any.

We want to collect all the paths where the alias is a wildcard and matches the import, and then append the delta to the mapped path.

Something like this would work:

const result = new Set<string>; // set ensures there are no duplicates

Object.entries(paths).forEach((alias, targets) => {
  if (!alias.endsWith('*')) {
    return;
  }
  const strippedAlias = alias.slice(0, -1); // remove asterisk
  if (!importScope.startsWith(strippedAlias)) {
    return;
  }
  const dynamicPart = importScope.slice(strippedAlias.length);
  targets.forEach(target => {
    result.add(target.replace('*', dynamicPart)); // add interpolated value
  })
})

return Array.from(result);

I think this is cleaner and more readable, but most importantly - using primitive functions over higher-order functions (map, filter, flat) improves performance significantly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used your implementation and validated it against the older code. Looks correct. I took liberty to convert .forEach( into for ... of ... to be in line with removing higher-order functions from the computation.

Copy link
Contributor

Choose a reason for hiding this comment

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

The .forEach is not a higher-order function and is slightly faster than for...of and comparable with plain for. But this is not a deal breaker so let's just leave it.

const JS_SUFFIX = '.js';
const stats = lstatSync(filePath, { throwIfNoEntry: false }); // TODO nodejs bug: can return undefined despite contract https://nodejs.org/api/fs.html#fslstatsyncpath-options
if (!stats) {
const suffixesToTry = [TSX_SUFFIX, TS_SUFFIX, JSX_SUFFIX, JS_SUFFIX];
Copy link
Contributor

Choose a reason for hiding this comment

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

Please replace this recursive logic with:

if (lstatSync(filePath).isDirectory()) {
  // existing unchanged code
} else if (!lstatSync(filePath, { throwIfNoEntry: false })){  // not folder, but not full file either
  // try to find an extension that exists
	const ext = ['.ts', '.tsx', '.js', '.jsx'].find(ext => lstatSync(filePath + ext, { throwIfNoEntry: false }));
  if (ext) {
    filePath += ext;
  }
}

// the rest of the logic unchanged

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced

Copy link
Contributor

@meeroslav meeroslav left a comment

Choose a reason for hiding this comment

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

The reasoning is correct, but code is unnecessarily complex. Check my comments and try to make it simpler.

@meeroslav meeroslav added the scope: linter Issues related to Eslint support in Nx label Jul 27, 2023
@dearlordylord
Copy link
Contributor Author

Thank you for your suggestions @meeroslav , appreciated it! I've updated the PR accordingly.

Copy link
Contributor

@meeroslav meeroslav left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you for the PR and for making the necessary changes.

@meeroslav meeroslav merged commit 9c7ded0 into nrwl:master Jul 31, 2023
15 checks passed
@dearlordylord dearlordylord deleted the feat/module-boundaries-lint-rule-wildcards branch July 31, 2023 18:29
@github-actions
Copy link

github-actions bot commented Aug 6, 2023

This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
scope: linter Issues related to Eslint support in Nx
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants