Skip to content

Conversation

@chase
Copy link

@chase chase commented Oct 18, 2025

Extends the symlink support in GetEachFileNameOfModule to properly resolve module specifiers across symlinked packages and workspaces.

Key changes:

  • Move knownsymlinks from compiler to dedicated symlinks package
  • Implement active resolution via ResolveModuleName to populate cache
  • Add dependency resolution from package.json to detect symlinks early
  • Improve ignored path handling (node_modules/., .git, .# emacs locks)
  • Add comprehensive test coverage for symlink resolution
  • Fix declaration emit to prefer original paths over symlink paths

Fixes #1657 and #1034 (comment)
Fixes #1347

@chase
Copy link
Author

chase commented Oct 18, 2025

My intention is to help fix #1034, which is the only thing left blocking my team from adopting tsgo.

I'm not very experienced with Go, but I wanted to expand upon @shinichy's work here: https://github.com/shinichy/typescript-go/tree/symlinks

@neo773
Copy link

neo773 commented Oct 18, 2025

We have the same issue in our team that's stopping us from adopting tsgo

So I compiled your branch and ran a test, it doesn't seem to make any difference in our code base. Just wanted to let you know.

neo@neos-MacBook-Pro twenty-server % /Users/neo/Desktop/typescript-go/built/local/tsgo          
src/engine/api/mcp/services/tools/mcp-metadata-tools.service.ts:90:9 - error TS2742: The inferred type of 'send' cannot be named without a reference to '../../../../../../../../node_modules/axios/index.d.cts'. This is likely not portable. A type annotation is necessary.

90   async send(requestContext: RequestContext, data: Query) {
           ~~~~

src/engine/api/rest/metadata/rest-api-metadata.service.ts:24:9 - error TS2742: The inferred type of 'get' cannot be named without a reference to '../../../../../../../node_modules/axios/index.d.cts'. This is likely not portable. A type annotation is necessary.

24   async get(request: Request) {
           ~~~

src/engine/api/rest/metadata/rest-api-metadata.service.ts:38:9 - error TS2742: The inferred type of 'create' cannot be named without a reference to '../../../../../../../node_modules/axios/index.d.cts'. This is likely not portable. A type annotation is necessary.

38   async create(request: Request) {
           ~~~~~~

src/engine/api/rest/metadata/rest-api-metadata.service.ts:52:9 - error TS2742: The inferred type of 'update' cannot be named without a reference to '../../../../../../../node_modules/axios/index.d.cts'. This is likely not portable. A type annotation is necessary.

52   async update(request: Request) {
           ~~~~~~

src/engine/api/rest/metadata/rest-api-metadata.service.ts:66:9 - error TS2742: The inferred type of 'delete' cannot be named without a reference to '../../../../../../../node_modules/axios/index.d.cts'. This is likely not portable. A type annotation is necessary.

66   async delete(request: Request) {
           ~~~~~~

src/engine/metadata-modules/constants/search-vector-field.constants.ts:3:14 - error TS2742: The inferred type of 'SEARCH_VECTOR_FIELD' cannot be named without a reference to '../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

3 export const SEARCH_VECTOR_FIELD = {
               ~~~~~~~~~~~~~~~~~~~

src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util.ts:5:14 - error TS2742: The inferred type of 'formatConnectRecordNotFoundErrorMessage' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

5 export const formatConnectRecordNotFoundErrorMessage = (
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/prefill-views.ts:21:14 - error TS2742: The inferred type of 'prefillViews' cannot be named without a reference to '../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

21 export const prefillViews = async (
                ~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/companies-all.view.ts:12:14 - error TS2742: The inferred type of 'companiesAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

12 export const companiesAllView = (
                ~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/custom-all.view.ts:6:14 - error TS2742: The inferred type of 'customAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

6 export const customAllView = (
               ~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/dashboards-all.view.ts:11:14 - error TS2742: The inferred type of 'dashboardsAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

11 export const dashboardsAllView = (
                ~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view.ts:10:14 - error TS2742: The inferred type of 'notesAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

10 export const notesAllView = (
                ~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/opportunities-all.view.ts:8:14 - error TS2742: The inferred type of 'opportunitiesAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

8 export const opportunitiesAllView = (
               ~~~~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts:8:14 - error TS2742: The inferred type of 'opportunitiesByStageView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

8 export const opportunitiesByStageView = (
               ~~~~~~~~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-table-by-stage.view.ts:8:14 - error TS2742: The inferred type of 'opportunitiesTableByStageView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

8 export const opportunitiesTableByStageView = (
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts:11:14 - error TS2742: The inferred type of 'peopleAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

11 export const peopleAllView = (
                ~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view.ts:10:14 - error TS2742: The inferred type of 'tasksAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

10 export const tasksAllView = (
                ~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me.ts:11:14 - error TS2742: The inferred type of 'tasksAssignedToMeView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

11 export const tasksAssignedToMeView = (
                ~~~~~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts:10:14 - error TS2742: The inferred type of 'tasksByStatusView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

10 export const tasksByStatusView = (
                ~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view.ts:8:14 - error TS2742: The inferred type of 'workflowRunsAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

8 export const workflowRunsAllView = (
               ~~~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view.ts:11:14 - error TS2742: The inferred type of 'workflowVersionsAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

11 export const workflowVersionsAllView = (
                ~~~~~~~~~~~~~~~~~~~~~~~

src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view.ts:11:14 - error TS2742: The inferred type of 'workflowsAllView' cannot be named without a reference to '../../../../../../../node_modules/@lingui/core/dist/index.d.cts'. This is likely not portable. A type annotation is necessary.

11 export const workflowsAllView = (
                ~~~~~~~~~~~~~~~~

src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service.ts:14:28 - error TS2503: Cannot find namespace 'DOMPurify'.

14   private readonly purify: DOMPurify.DOMPurify;
                              ~~~~~~~~~

src/utils/image.ts:27:18 - error TS2749: 'Axios' refers to a value, but is being used as a type here. Did you mean 'typeof Axios'?

27   axiosInstance: Axios,
                    ~~~~~

test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts:1:23 - error TS2307: Cannot find module '@faker-js/faker/.' or its corresponding type declarations.

1 import { faker } from '@faker-js/faker/.';
                        ~~~~~~~~~~~~~~~~~~~


Found 25 errors in 22 files.

Errors  Files
     1  src/engine/api/mcp/services/tools/mcp-metadata-tools.service.ts:90
     4  src/engine/api/rest/metadata/rest-api-metadata.service.ts:24
     1  src/engine/metadata-modules/constants/search-vector-field.constants.ts:3
     1  src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util.ts:5
     1  src/engine/workspace-manager/standard-objects-prefill-data/prefill-views.ts:21
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/companies-all.view.ts:12
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/custom-all.view.ts:6
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/dashboards-all.view.ts:11
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view.ts:10
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/opportunities-all.view.ts:8
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts:8
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-table-by-stage.view.ts:8
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts:11
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view.ts:10
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me.ts:11
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts:10
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view.ts:8
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view.ts:11
     1  src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view.ts:11
     1  src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service.ts:14
     1  src/utils/image.ts:27
     1  test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts:1

neo@neos-MacBook-Pro twenty-server % 

chase added 3 commits October 18, 2025 20:19
Extends the symlink support in GetEachFileNameOfModule to properly
resolve module specifiers across symlinked packages and workspaces.

Key changes:
- Move knownsymlinks from compiler to dedicated symlinks package
- Implement active resolution via ResolveModuleName to populate cache
- Add dependency resolution from package.json to detect symlinks early
- Improve ignored path handling (node_modules/., .git, .# emacs locks)
- Add comprehensive test coverage for symlink resolution
- Fix declaration emit to prefer original paths over symlink paths

This aligns with upstream TypeScript's symlink resolution behavior,
ensuring correct module specifiers in declaration files for monorepos
and symlinked dependencies.

Fixes baseline mismatches in:
- declarationEmitReexportedSymlinkReference2/3
- symlinkedWorkspaceDependencies* tests
- nodeModuleReexportFromDottedPath
Optimizes populateSymlinkCacheFromResolutions to avoid redundant
dependency resolution. Previously, every module specifier generation
would re-resolve all package.json dependencies. Now uses package-level
caching to resolve once and reuse results.

Performance improvements (measured with benchmarks):
- Speed: 9.28x faster (89.2% reduction: 509µs → 55µs per operation)
- Memory: 8.64x less (88.4% reduction: 597KB → 69KB)
- Allocations: 9.22x fewer (89.2% reduction: 12,177 → 1,321)

Key changes:
- Add package-level cache tracking in KnownSymlinks
- Eliminate intermediate slice allocations
- Reduce redundant ToPath() calls
- Add comprehensive benchmarks for symlink operations

For a project with 50 dependencies and 100 files, this saves multiple
seconds of compilation time by avoiding 5,000+ redundant resolutions.
@chase
Copy link
Author

chase commented Oct 18, 2025

We have the same issue in our team that's stopping us from adopting tsgo

So I compiled your branch and ran a test, it doesn't seem to make any difference in our code base. Just wanted to let you know.

Interesting. This is what I get in Linux after setting up your repo and replacing tsc in node_modules/.bin so that nx uses it instead:

src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service.ts(14,28): error TS2503: Cannot find namespace 'DOMPurify'.
src/utils/image.ts(27,18): error TS2749: 'Axios' refers to a value, but is being used as a type here. Did you mean 'typeof Axios'?
test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts(1,23): error TS2307: Cannot find module '@faker-js/faker/.' or its corresponding type declarations.

Seems like this might need some fixes for macOS.

@chase
Copy link
Author

chase commented Oct 19, 2025

@neo773 should be fixed with the latest commit, the above errors seem like they might be actual errors or at least ones that can be fixed trivially:

Patch

diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service.ts
index f81f151..335fe77 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service.ts
@@ -8,10 +8,12 @@ import * as planer from 'planer';
 
 import { safeDecodeURIComponent } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/safe-decode-uri-component.util';
 
+type DOMPurifyInstance = ReturnType<typeof DOMPurify>;
+
 @Injectable()
 export class ImapMessageTextExtractorService {
   private readonly jsdomInstance: JSDOM;
-  private readonly purify: DOMPurify.DOMPurify;
+  private readonly purify: DOMPurifyInstance;
 
   constructor() {
     this.jsdomInstance = new JSDOM('');
diff --git a/packages/twenty-server/src/utils/image.ts b/packages/twenty-server/src/utils/image.ts
index 8e98729..ad78167 100644
--- a/packages/twenty-server/src/utils/image.ts
+++ b/packages/twenty-server/src/utils/image.ts
@@ -1,4 +1,4 @@
-import { type Axios } from 'axios';
+import { type AxiosInstance } from 'axios';
 
 const cropRegex = /([w|h])([0-9]+)/;
 
@@ -24,7 +24,7 @@ export const getCropSize = (value: ShortCropSize): CropSize | null => {
 
 export const getImageBufferFromUrl = async (
   url: string,
-  axiosInstance: Axios,
+  axiosInstance: AxiosInstance,
 ): Promise<Buffer> => {
   const response = await axiosInstance.get(url, {
     responseType: 'arraybuffer',
diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts
index 0aa4bb7..2981c77 100644
--- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts
+++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts
@@ -1,4 +1,4 @@
-import { faker } from '@faker-js/faker/.';
+import { faker } from '@faker-js/faker';
 import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
 import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
 import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';

@neo773
Copy link

neo773 commented Oct 19, 2025

@chase
With the new commit it finally works, as for the remaining type errors they don't seem to affect Strada version, but it doesn't matters.

Thank you for your work on this.

@tmm1
Copy link
Contributor

tmm1 commented Oct 19, 2025

Thanks for working on this!

Fixes #1034 (comment) and #1657 also

@chase chase changed the title Improve symlink resolution in module specifier generation Fix #1034: Improve symlink resolution in module specifier generation Oct 21, 2025
@AlCalzone
Copy link
Contributor

Uhh, this is great! It seems that this fixes my issue #1347 too.

@chase could you include the compiler test case I posted in that issue?

@chase chase requested review from iisaduan and jakebailey October 24, 2025 14:04
// if p.Host().GetSymlinkCache() != nil {
// return p.Host().GetSymlinkCache()
// }
if p.knownSymlinks == nil {
Copy link
Member

Choose a reason for hiding this comment

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

This condition looks to be impossible as-written, but I think lazy initialization is a good idea. However, you need to guard the field initialization with a sync.Once like the other lazy computed caches.

Comment on lines +1649 to +1650
// In declaration-only builds, the symlink cache might not be populated yet
// because module resolution was skipped. Populate it now if we have resolutions.
Copy link
Member

Choose a reason for hiding this comment

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

I don’t recall exactly how this happened in Strada, but I don’t think this comment applies in Corsa.

Copy link
Author

@chase chase Oct 28, 2025

Choose a reason for hiding this comment

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

This occurs regularly in pnpm workspaces when running tsgo --build --emitDeclarationsOnly, if I recall correctly.

// Helper to resolve dependencies without creating intermediate slices
resolveDeps := func(deps map[string]string) {
for depName := range deps {
resolved := host.ResolveModuleName(depName, packageJsonPath, options.OverrideImportMode)
Copy link
Member

Choose a reason for hiding this comment

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

I’d like to get this out of here by frontloading these resolutions to program construction. That will allow us to remove ResolveModuleName from the host interfaces and should also remove all concurrent writes from the symlink cache. In moving it there, I also think the program could do a better job of determining which of these resolutions are necessary and avoid doing redundant ones. The original comment that said most of these resolutions will already be cached is not currently true in Corsa.

It is likely the case that we get 90% of the way there without this block of logic, as this was added to Strada long after the symlink cache existed with only the resolutions it made during program construction. Dropping it from this PR and leaving it as a to-do would be fine. The priority is to eliminate file system reads and cache mutations post-program-load.

Copy link
Author

Choose a reason for hiding this comment

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

I'm worried that since the current test suite don't adequately cover handling pnpm workspaces, that removing this and relocating some of this willl cause a regression for some of the cases that were solved for others.

I might have time to work on this over the weekend, but if your or another wants to pick up this branch and get it up to standard, I don't want to to block getting a fix merged.

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

Labels

None yet

Projects

None yet

7 participants