From 88645e0e4e6d2b8fd053d75f39260687a733e435 Mon Sep 17 00:00:00 2001 From: sumeyye Date: Wed, 15 Oct 2025 17:00:29 +0300 Subject: [PATCH 1/4] add: a post for explaining the ways of path mapping --- .../POST.md | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md diff --git a/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md b/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md new file mode 100644 index 0000000000..76d04f8a67 --- /dev/null +++ b/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md @@ -0,0 +1,211 @@ +# Angular Library Linking Made Easy: Paths, Workspaces, and Symlinks + +### Understanding TypeScript Path Mapping + +Path aliases is a powerful feature in TypeScript that helps developers simplify and organize their import statements. Instead of dealing with long and error-prone relative paths like `../../../components/button`, you can define a clear and descriptive alias that points directly to a specific directory or module. + +This configuration is managed through the `paths` property in the TypeScript configuration file (`tsconfig.json`), allowing you to map custom names to local folders or compiled outputs. For example: + +```tsx +// tsconfig.json +{ + "compilerOptions": { + "paths": { + "@my-package": ["./dist/my-package"], + "@my-second-package": ["./projects/my-second-package/src/public-api.ts"] + + } + } +} +``` + +In this setup, `@my-package` serves as a shorthand reference to your locally built library. Once configured, you can import modules using `@my-package` instead of long relative paths, which greatly improves readability and maintainability across large projects. + +When working with multiple subdirectories or a more complex folder structure, you can also use wildcards to create flexible and dynamic mappings. This pattern is especially useful for modular libraries or mono-repos that contain multiple sub-packages: + +```tsx +// tsconfig.json +{ + "compilerOptions": { + "paths": { + "@my-package/*": ["./dist/my-package/*"] + } + } +} +``` + +With this approach, imports like `@my-package/utils` or `@my-package/components/button` will automatically resolve to the corresponding directories in your build output. This makes your codebase more maintainable, portable, and consistent. This is useful especially when collaborating across teams or working with multiple libraries in the same workspace. + +--- + +### Step-by-Step Examples of Path Configuration + +As this example provides a glimpse for the path mapping, this is not the only way for aliases. Here are the other ways to utilize this feature. + +1. **Using `package.json` Exports for Library Mapping** + + When developing internal libraries within a mono-repo, another option is to use the `exports` field in each library’s `package.json` + + This allows Node and modern bundlers to resolve imports cleanly when consuming the library, without depending solely on TypeScript configuration. + + ```tsx + // dist/my-lib/package.json + { + "name": "@my-org/my-lib", + "version": "1.0.0", + "exports": { + ".": "./index.js", + "./utils": "./utils/index.ts" + } + } + ``` + + ```tsx + import { formatDate } from "@my-org/my-lib/utils"; + ``` + + This approach becomes especially powerful when publishing your libraries or integrating them into larger Angular mono-repos. Because, it aligns both runtime (Node) and compile-time (TypeScript) resolution. + +2. **Linking Local Libraries via Symlinks** + + If you want to use a local library that is not yet published to npm, you can create a symbolic link between your library’s `dist` output and your consuming app. + + This is useful when testing or developing multiple packages in parallel. + + You can create a symlink using npm or yarn: + + ```tsx + # Inside your library folder + npm link + + # Inside your consuming app + npm link @my-org/my-lib + ``` + + This effectively tells Node to resolve `@my-org/my-lib` from your local file system instead of the npm registry. + + However, note that symlinks can sometimes lead to path resolution issues with certain Angular build configurations, especially before the new application builder. With the latest builder improvements, this approach is becoming more stable and predictable. + +3. **Combining Path Mapping with Workspace Configuration** + + In a structured Angular workspace, especially one created with **Nx** or **Angular CLI** using multiple projects, you can combine the approaches above. + + For instance, your `tsconfig.base.json` can define local references for in-repo libraries, while each library’s `package.json` provides external mappings for reuse outside the workspace. + + This hybrid setup ensures that: + + - The workspace remains easy to navigate and refactor locally. + - External consumers (or CI builds) can still resolve imports correctly once libraries are built. + + For larger Angular projects or mono-repos, **Workspaces** (supported by both **Yarn** and **npm**) offer a clean way to manage multiple local packages within the same repository. Workspaces automatically link internal libraries together, so you can reference them by name instead of using manual `file:` paths or complex TypeScript aliases. This approach keeps dependencies consistent, simplifies cross-project development, and scales well for enterprise or multi-package setups. + +Each of these methods has its strengths: + +- **TypeScript paths:** This is great for local development and quick imports. +- **`package.json` exports:** This is ideal for libraries meant to be distributed. +- **Symlinks:** These are convenient for local testing between projects. + +Choosing the right one, or even combining them depends on the scale of your project and whether you are building internal libraries, or a full mono-repo setup. + +--- + +### How Path References Worked Before the New Angular Application Builder + +Angular used to support path aliases to the locally installed packages by referencing to the `node_modules` folder like this: + +```tsx +// tsconfig.json +{ + "compilerOptions": { + "paths": { + "@angular/*": ["./node_modules/@angular/*"] + } + } +} +``` + +However, this approach is not recommended, hence not supported, by the typescript. You can find detailed guidance on this topic in the TypeScript documentation, which notes that paths should not reference mono-repo packages or those inside **node_modules\***:\* [Paths should not point to monorepo packages or node_modules packages](https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages). + +Giving a real life example would explain the situation better. Suppose that you have such structure: + +- You have a main angular app that consumes several npm dependencies and holds registered local paths that reference to another library locally like this: + ```tsx + // angular/tsconfig.json + { + "compileOnSave": false, + "compilerOptions": { + "paths": { + "@abp/ng.identity": [ + "../modules/Volo.Abp.Identity/angular/projects/identity/src/public-api.ts" + ], + "@abp/ng.identity/config": [ + "../modules/Volo.Abp.Identity/angular/projects/identity/config/src/public-api.ts" + ], + "@abp/ng.identity/proxy": [ + "../modules/Volo.Abp.Identity/angular/projects/identity/proxy/src/public-api.ts" + ] + }, + }, + } + ``` + This simply references to this package physically https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages/identity +- This library is also using these dependencies + ```tsx + // npm/ng-packs/packages/identity/package.json + { + "name": "@abp/ng.identity", + "version": "10.0.0-rc.1", + "homepage": "https://abp.io", + "repository": { + "type": "git", + "url": "https://github.com/abpframework/abp.git" + }, + "dependencies": { + "@abp/ng.components": "~10.0.0-rc.1", + "@abp/ng.permission-management": "~10.0.0-rc.1", + "@abp/ng.theme.shared": "~10.0.0-rc.1", + "tslib": "^2.0.0" + }, + "publishConfig": { + "access": "public" + } + } + ``` + As these libraries also have their own dependencies, the identity package needs to consume them in itself. Before the [application builder migration](https://angular.dev/tools/cli/build-system-migration), you could register the path configuration like this + ```tsx + // angular/tsconfig.json + { + "compileOnSave": false, + "compilerOptions": { + "paths": { + "@angular/*":["node_modules/@angular/*"], + "@abp/*":["node_modules/@abp/*"], + "@swimlane/*": ["node_modules/@swimlane/*"], + "@ngx-validate/core":["node_modules/@ngx-validate/core"], + "@ng-bootstrap/ng-bootstrap": ["node_modules/@ng-bootstrap/ng-bootstrap"], + "@abp/ng.identity": [ + "../modules/Volo.Abp.Identity/angular/projects/identity/src/public-api.ts" + ], + "@abp/ng.identity/config": [ + "../modules/Volo.Abp.Identity/angular/projects/identity/config/src/public-api.ts" + ], + "@abp/ng.identity/proxy": [ + "../modules/Volo.Abp.Identity/angular/projects/identity/proxy/src/public-api.ts" + ] + }, + }, + } + ``` + However, the latest builder forces more strict rules.So, it does not resolve the paths that reference to the `node_modules` causing a common DI errors as mentioned here: + - https://github.com/angular/angular-cli/issues/31395 + - https://github.com/angular/angular-cli/issues/26901 + - https://github.com/angular/angular-cli/issues/27176 + +In this case, we recommend using a symlink script. You can reach them through: [INSERT GITHUB LINK] + +These scripts help you share dependencies from the main Angular app to local library projects via symlinks: + +- `symlink-config.ps1` centralizes which library directories to touch (e.g., ../../modules/Volo.Abp.Identity/angular/projects/identity) and which packages to link (e.g., @angular, @abp, rxjs) +- `setup-symlinks.ps1` reads that config and, for each library, creates a `node_modules` folder if needed and symlinks only the listed packages from the `node_modules` of the app to avoid duplicate installs +- `remove-symlinks.ps1` cleans up by deleting those library node_modules directories so they can use their own local deps again +- In `angular/package.json`, the `symlinks:setup` and `symlinks:remove` npm scripts simply run those two PowerShell scripts so you can execute them conveniently with your package manager. From b6c32a709ff3c93d875833fcb47c24fae3956445 Mon Sep 17 00:00:00 2001 From: sumeyye Date: Thu, 16 Oct 2025 10:20:37 +0300 Subject: [PATCH 2/4] update: add intro, closing and sample application link --- .../POST.md | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md b/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md index 76d04f8a67..62c52120ea 100644 --- a/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md +++ b/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md @@ -1,5 +1,7 @@ # Angular Library Linking Made Easy: Paths, Workspaces, and Symlinks +Managing local libraries and path references in Angular projects has evolved significantly with the introduction of the new Angular application builder. What once required manual path mappings, fragile symlinks, and `node_modules` references is now more structured, predictable, and aligned with modern TypeScript and workspace practices. This guide walks through how path mapping works, how it has changed, and the best ways to link and manage your local libraries in brand new Angular ecosystem. + ### Understanding TypeScript Path Mapping Path aliases is a powerful feature in TypeScript that helps developers simplify and organize their import statements. Instead of dealing with long and error-prone relative paths like `../../../components/button`, you can define a clear and descriptive alias that points directly to a specific directory or module. @@ -40,7 +42,7 @@ With this approach, imports like `@my-package/utils` or `@my-package/components/ ### Step-by-Step Examples of Path Configuration -As this example provides a glimpse for the path mapping, this is not the only way for aliases. Here are the other ways to utilize this feature. +As this example provides a glimpse for the path mapping, this is not the only way for the aliases. Here are the other ways to utilize this feature. 1. **Using `package.json` Exports for Library Mapping** @@ -74,7 +76,7 @@ As this example provides a glimpse for the path mapping, this is not the only wa You can create a symlink using npm or yarn: - ```tsx + ```bash # Inside your library folder npm link @@ -124,11 +126,12 @@ Angular used to support path aliases to the locally installed packages by refere } ``` -However, this approach is not recommended, hence not supported, by the typescript. You can find detailed guidance on this topic in the TypeScript documentation, which notes that paths should not reference mono-repo packages or those inside **node_modules\***:\* [Paths should not point to monorepo packages or node_modules packages](https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages). +However, this approach is not recommended, hence not supported, by the TypeScript. You can find detailed guidance on this topic in the TypeScript documentation, which notes that paths should not reference mono-repo packages or those inside **node_modules\***:\* [Paths should not point to monorepo packages or node_modules packages](https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages). Giving a real life example would explain the situation better. Suppose that you have such structure: -- You have a main angular app that consumes several npm dependencies and holds registered local paths that reference to another library locally like this: +- Amain angular app that consumes several npm dependencies and holds registered local paths that reference to another library locally like this: + ```tsx // angular/tsconfig.json { @@ -148,8 +151,11 @@ Giving a real life example would explain the situation better. Suppose that you }, } ``` + This simply references to this package physically https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages/identity + - This library is also using these dependencies + ```tsx // npm/ng-packs/packages/identity/package.json { @@ -171,7 +177,9 @@ Giving a real life example would explain the situation better. Suppose that you } } ``` + As these libraries also have their own dependencies, the identity package needs to consume them in itself. Before the [application builder migration](https://angular.dev/tools/cli/build-system-migration), you could register the path configuration like this + ```tsx // angular/tsconfig.json { @@ -196,16 +204,40 @@ Giving a real life example would explain the situation better. Suppose that you }, } ``` - However, the latest builder forces more strict rules.So, it does not resolve the paths that reference to the `node_modules` causing a common DI errors as mentioned here: + + However, the latest builder forces more strict rules. So, it does not resolve the paths that reference to the `node_modules` causing a common DI error as mentioned here: + - https://github.com/angular/angular-cli/issues/31395 - https://github.com/angular/angular-cli/issues/26901 - https://github.com/angular/angular-cli/issues/27176 -In this case, we recommend using a symlink script. You can reach them through: [INSERT GITHUB LINK] +In this case, we recommend using a symlink script. You can reach them through this example application: [🔗 Angular Sample Path Reference](https://github.com/sumeyyeKurtulus/AbpPathReferenceExamples) These scripts help you share dependencies from the main Angular app to local library projects via symlinks: - `symlink-config.ps1` centralizes which library directories to touch (e.g., ../../modules/Volo.Abp.Identity/angular/projects/identity) and which packages to link (e.g., @angular, @abp, rxjs) - `setup-symlinks.ps1` reads that config and, for each library, creates a `node_modules` folder if needed and symlinks only the listed packages from the `node_modules` of the app to avoid duplicate installs -- `remove-symlinks.ps1` cleans up by deleting those library node_modules directories so they can use their own local deps again +- `remove-symlinks.ps1` cleans up by deleting those library `node_modules` directories so they can use their own local deps again - In `angular/package.json`, the `symlinks:setup` and `symlinks:remove` npm scripts simply run those two PowerShell scripts so you can execute them conveniently with your package manager. + +--- + +### Best Practices and Recommendations + +As we have explained each way of path mapping, this part of the article aims to summarize the best practices. Here are the points you need to consider: + +- Prefer **workspace references** for large projects and mono-repos. +- Use **TypeScript path aliases** only for local development convenience. +- Strictly avoid referencing `node_modules` directly; let the Angular builder manage package resolution. +- Maintain **consistent library structures** with clear `package.json` exports for reusable libraries. +- Automate **symlink creation/removal** if needed to reduce manual errors. + +Here is the list of common pitfalls and how you could troubleshoot them: + +- **DI errors after path configurations for typescript config**: Ensure that only one copy of each library is resolved. Avoid duplicate modules by checking `node_modules` and symlinks. +- **IDE not recognizing aliases**: Confirm that `tsconfig.json` or `tsconfig.base.json` includes the correct `paths` configuration and that your IDE is using the correct tsconfig. +- **Build errors with old paths**: Migrate paths pointing to `node_modules` to either workspace references or local library paths. +- **Symlink issues in CI/CD**: Use automated scripts to create/remove symlinks consistently; do not rely on manual linking. +- **Module resolution conflicts**: Check library dependencies for mismatched versions and align them using a package manager workspace strategy. + +As Angular’s build system continues to mature, developers are encouraged to move away from outdated path configurations and manual symlink setups. By embracing workspace references, consistent library exports, and TypeScript path mapping, teams can build scalable, maintainable applications without wrestling with complex import paths or dependency conflicts. With the right configuration, local development becomes faster, cleaner, and far more reliable. From 2e575c2120aa1fa19920b2a3681422db48367857 Mon Sep 17 00:00:00 2001 From: sumeyye Date: Thu, 16 Oct 2025 10:24:51 +0300 Subject: [PATCH 3/4] fix: a typo --- .../POST.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md b/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md index 62c52120ea..c94e2c2cf8 100644 --- a/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md +++ b/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md @@ -126,7 +126,7 @@ Angular used to support path aliases to the locally installed packages by refere } ``` -However, this approach is not recommended, hence not supported, by the TypeScript. You can find detailed guidance on this topic in the TypeScript documentation, which notes that paths should not reference mono-repo packages or those inside **node_modules\***:\* [Paths should not point to monorepo packages or node_modules packages](https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages). +However, this approach is not recommended, hence not supported, by the TypeScript. You can find detailed guidance on this topic in the TypeScript documentation, which notes that paths should not reference mono-repo packages or those inside **node_modules**: [Paths should not point to monorepo packages or node_modules packages](https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages). Giving a real life example would explain the situation better. Suppose that you have such structure: From 65175a415124094495ef60b2eab45a8b285e9563 Mon Sep 17 00:00:00 2001 From: sumeyye Date: Fri, 17 Oct 2025 08:08:40 +0300 Subject: [PATCH 4/4] fix: code block language definitions --- .../POST.md | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md b/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md index c94e2c2cf8..52601e8624 100644 --- a/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md +++ b/docs/en/Community-Articles/2025-10-15-angular-library-linking-made-easy-paths-workspaces-and-symlinks/POST.md @@ -8,16 +8,15 @@ Path aliases is a powerful feature in TypeScript that helps developers simplify This configuration is managed through the `paths` property in the TypeScript configuration file (`tsconfig.json`), allowing you to map custom names to local folders or compiled outputs. For example: -```tsx +```json // tsconfig.json { - "compilerOptions": { - "paths": { - "@my-package": ["./dist/my-package"], - "@my-second-package": ["./projects/my-second-package/src/public-api.ts"] - - } - } + "compilerOptions": { + "paths": { + "@my-package": ["./dist/my-package"], + "@my-second-package": ["./projects/my-second-package/src/public-api.ts"] + } + } } ``` @@ -25,14 +24,14 @@ In this setup, `@my-package` serves as a shorthand reference to your locally bui When working with multiple subdirectories or a more complex folder structure, you can also use wildcards to create flexible and dynamic mappings. This pattern is especially useful for modular libraries or mono-repos that contain multiple sub-packages: -```tsx +```json // tsconfig.json { - "compilerOptions": { - "paths": { - "@my-package/*": ["./dist/my-package/*"] - } - } + "compilerOptions": { + "paths": { + "@my-package/*": ["./dist/my-package/*"] + } + } } ``` @@ -50,7 +49,7 @@ As this example provides a glimpse for the path mapping, this is not the only wa This allows Node and modern bundlers to resolve imports cleanly when consuming the library, without depending solely on TypeScript configuration. - ```tsx + ```json // dist/my-lib/package.json { "name": "@my-org/my-lib", @@ -115,14 +114,14 @@ Choosing the right one, or even combining them depends on the scale of your proj Angular used to support path aliases to the locally installed packages by referencing to the `node_modules` folder like this: -```tsx +```json // tsconfig.json { - "compilerOptions": { - "paths": { - "@angular/*": ["./node_modules/@angular/*"] - } - } + "compilerOptions": { + "paths": { + "@angular/*": ["./node_modules/@angular/*"] + } + } } ``` @@ -132,7 +131,7 @@ Giving a real life example would explain the situation better. Suppose that you - Amain angular app that consumes several npm dependencies and holds registered local paths that reference to another library locally like this: - ```tsx + ```json // angular/tsconfig.json { "compileOnSave": false, @@ -147,8 +146,8 @@ Giving a real life example would explain the situation better. Suppose that you "@abp/ng.identity/proxy": [ "../modules/Volo.Abp.Identity/angular/projects/identity/proxy/src/public-api.ts" ] - }, - }, + } + } } ``` @@ -156,7 +155,7 @@ Giving a real life example would explain the situation better. Suppose that you - This library is also using these dependencies - ```tsx + ```json // npm/ng-packs/packages/identity/package.json { "name": "@abp/ng.identity", @@ -180,17 +179,19 @@ Giving a real life example would explain the situation better. Suppose that you As these libraries also have their own dependencies, the identity package needs to consume them in itself. Before the [application builder migration](https://angular.dev/tools/cli/build-system-migration), you could register the path configuration like this - ```tsx + ```json // angular/tsconfig.json { "compileOnSave": false, "compilerOptions": { "paths": { - "@angular/*":["node_modules/@angular/*"], - "@abp/*":["node_modules/@abp/*"], + "@angular/*": ["node_modules/@angular/*"], + "@abp/*": ["node_modules/@abp/*"], "@swimlane/*": ["node_modules/@swimlane/*"], - "@ngx-validate/core":["node_modules/@ngx-validate/core"], - "@ng-bootstrap/ng-bootstrap": ["node_modules/@ng-bootstrap/ng-bootstrap"], + "@ngx-validate/core": ["node_modules/@ngx-validate/core"], + "@ng-bootstrap/ng-bootstrap": [ + "node_modules/@ng-bootstrap/ng-bootstrap" + ], "@abp/ng.identity": [ "../modules/Volo.Abp.Identity/angular/projects/identity/src/public-api.ts" ], @@ -200,8 +201,8 @@ Giving a real life example would explain the situation better. Suppose that you "@abp/ng.identity/proxy": [ "../modules/Volo.Abp.Identity/angular/projects/identity/proxy/src/public-api.ts" ] - }, - }, + } + } } ```