From 172f4f70bb4748590f50b54278d034b4f84b0ae8 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Thu, 26 Feb 2026 18:04:00 -0800 Subject: [PATCH 1/3] Integrate Glint for virtual file TS transform (Phase 1) When @glint/ember-tsc is installed and a glint environment is configured in tsconfig, use Glint's rewriteModule() instead of transformForLint() to generate virtual .mts/.mjs file contents in the TS patch. This gives TypeScript proper type understanding of imported .gts/.gjs modules. Falls back to the existing transformForLint path when Glint is unavailable (not installed, Node < 22.12, no glint config, or transform error). Co-Authored-By: Claude Opus 4.6 --- package.json | 6 +- pnpm-lock.yaml | 351 ++++++++++++++++++++++-- src/parser/glint-utils.js | 57 ++++ src/parser/ts-patch.js | 28 +- test-projects/gts-glint/.eslintignore | 1 + test-projects/gts-glint/.eslintrc.cjs | 22 ++ test-projects/gts-glint/index.d.ts | 1 + test-projects/gts-glint/package.json | 21 ++ test-projects/gts-glint/src/consumer.ts | 5 + test-projects/gts-glint/src/greeter.gts | 19 ++ test-projects/gts-glint/tsconfig.json | 19 ++ 11 files changed, 504 insertions(+), 26 deletions(-) create mode 100644 src/parser/glint-utils.js create mode 100644 test-projects/gts-glint/.eslintignore create mode 100644 test-projects/gts-glint/.eslintrc.cjs create mode 100644 test-projects/gts-glint/index.d.ts create mode 100644 test-projects/gts-glint/package.json create mode 100644 test-projects/gts-glint/src/consumer.ts create mode 100644 test-projects/gts-glint/src/greeter.gts create mode 100644 test-projects/gts-glint/tsconfig.json diff --git a/package.json b/package.json index 30d9f2d..53797c9 100644 --- a/package.json +++ b/package.json @@ -59,16 +59,20 @@ }, "peerDependencies": { "@babel/core": "^7.23.6", + "@glint/ember-tsc": ">= 1.1.0", "@typescript-eslint/parser": "*" }, "peerDependenciesMeta": { + "@glint/ember-tsc": { + "optional": true + }, "@typescript-eslint/parser": { "optional": true } }, "packageManager": "pnpm@10.21.0", "engines": { - "node": ">=16.0.0" + "node": ">=22.12.0" }, "pnpm": { "overrides": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad52d6a..fc0e429 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@glimmer/syntax': specifier: '>= 0.92.0' version: 0.92.3 + '@glint/ember-tsc': + specifier: '>= 1.1.0' + version: 1.1.1(typescript@5.7.2) '@typescript-eslint/tsconfig-utils': specifier: ^8.38.0 version: 8.38.0(typescript@5.7.2) @@ -141,7 +144,7 @@ importers: version: 3.0.8 ember-source: specifier: ^6.1.0 - version: 6.1.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.94.0) + version: 6.1.0(@glimmer/component@2.0.0)(@glint/template@1.7.4)(rsvp@4.8.5)(webpack@5.94.0) eslint: specifier: ^9.17.0 version: 9.17.0 @@ -335,6 +338,45 @@ importers: specifier: ^8.19.1 version: 8.19.1(eslint@8.57.1)(typescript@5.7.2) + test-projects/gts-glint: + devDependencies: + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0 + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@glimmer/tracking': + specifier: ^1.1.2 + version: 1.1.2 + '@glint/ember-tsc': + specifier: ^1.1.0 + version: 1.1.1(typescript@5.7.2) + '@glint/template': + specifier: ^1.3.0 + version: 1.5.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.46.4 + version: 8.46.4(@typescript-eslint/parser@8.46.4(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) + '@typescript-eslint/parser': + specifier: ^8.46.4 + version: 8.46.4(eslint@8.57.1)(typescript@5.7.2) + ember-eslint-parser: + specifier: workspace:* + version: link:../.. + ember-source: + specifier: ^5.6.0 + version: 5.12.0(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.94.0) + eslint: + specifier: ^8.0.1 + version: 8.57.1 + eslint-plugin-ember: + specifier: ^12.0.0 + version: 12.3.3(@typescript-eslint/parser@8.46.4(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1) + typescript: + specifier: ^5.3.3 + version: 5.7.2 + test-projects/rules/padding-line-between-statements: devDependencies: '@typescript-eslint/eslint-plugin': @@ -1309,9 +1351,18 @@ packages: '@glimmer/wire-format@0.94.8': resolution: {integrity: sha512-A+Cp5m6vZMAEu0Kg/YwU2dJZXyYxVJs2zI57d3CP6NctmX7FsT8WjViiRUmt5abVmMmRH5b8BUovqY6GSMAdrw==} + '@glint/ember-tsc@1.1.1': + resolution: {integrity: sha512-SEIyDPOv9nKpoXaRWp6rXrAnZu75GXW3MVg9nmxX0bwc0s2Aydpd/T0YjZux1ZJ0v8YevmFkBjlxk3UiSU3a6g==} + hasBin: true + peerDependencies: + typescript: '>=5.6.0' + '@glint/template@1.5.0': resolution: {integrity: sha512-KyQUCWifxl8wDxo3SXzJcGKttHbIPgFBtqsoiu13Edx/o4CgGXr5rrM64jJR7Wvunn8sRM+Rq7Y0cHoB068Wuw==} + '@glint/template@1.7.4': + resolution: {integrity: sha512-39gTESXJmiIzJhcweJQ+44eIX+n+alJpD6HKpX8nPXCggVu2Yq6KP9pA5gwUvWE1/NYZhITiOqdA7UuyVtWMww==} + '@handlebars/parser@2.0.0': resolution: {integrity: sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==} @@ -1820,6 +1871,32 @@ packages: '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + '@volar/kit@2.4.28': + resolution: {integrity: sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==} + peerDependencies: + typescript: '*' + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/language-server@2.4.28': + resolution: {integrity: sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==} + + '@volar/language-service@2.4.28': + resolution: {integrity: sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/test-utils@2.4.28': + resolution: {integrity: sha512-N7RNiHHDPtqK5B21x4W462XMQj7Z75ynN3isLP+3Rb44hbJjhxxDxzs+QqWB0sjM57EtTJga+SDd9WWy3OjMzA==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -2373,6 +2450,9 @@ packages: content-tag@2.0.3: resolution: {integrity: sha512-htLIdtfhhKW2fHlFLnZH7GFzHSdSpHhDLrWVswkNiiPMZ5uXq5JfrGboQKFhNQuAAFF8VNB2EYUj3MsdJrKKpg==} + content-tag@3.1.3: + resolution: {integrity: sha512-4Kiv9mEroxuMXfWUNUHcljVJgxThCNk7eEswdHMXdzJnkBBaYDqDwzHkoh3F74JJhfU3taJOsgpR6oEGIDg17g==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4142,6 +4222,9 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -4412,6 +4495,9 @@ packages: remove-types@1.0.0: resolution: {integrity: sha512-G7Hk1Q+UJ5DvlNAoJZObxANkBZGiGdp589rVcTW/tYqJWJ5rwfraSnKSQaETN8Epaytw8J40nS/zC7bcHGv36w==} + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4659,10 +4745,6 @@ packages: resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} - source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4905,9 +4987,6 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -4974,6 +5053,12 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + + typescript-auto-import-cache@0.3.6: + resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} + typescript-eslint@8.19.1: resolution: {integrity: sha512-LKPUQpdEMVOeKluHi8md7rwLcoXHhwvWp3x+sJkMuq3gGm9yaYJtPo8sRZSblMFJ5pcOGCAak/scKf1mvZDlQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5136,6 +5221,48 @@ packages: jsdom: optional: true + volar-service-html@0.0.70: + resolution: {integrity: sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.70: + resolution: {integrity: sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-html-languageservice@5.6.2: + resolution: {integrity: sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + walk-sync@0.3.4: resolution: {integrity: sha512-ttGcuHA/OBnN2pcM6johpYlEms7XpO5/fyKIr48541xXedan4roO8cS1Q2S/zbbjGH/BarYDAMeS2Mi9HE5Tig==} @@ -6064,7 +6191,7 @@ snapshots: '@babel/parser': 7.26.3 '@babel/template': 7.25.9 '@babel/types': 7.26.3 - debug: 4.4.0 + debug: 4.4.3 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6128,6 +6255,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@embroider/macros@1.16.10(@glint/template@1.7.4)': + dependencies: + '@embroider/shared-internals': 2.8.1 + assert-never: 1.4.0 + babel-import-util: 2.1.1 + ember-cli-babel: 7.26.11 + find-up: 5.0.0 + lodash: 4.17.21 + resolve: 1.22.10 + semver: 7.6.3 + optionalDependencies: + '@glint/template': 1.7.4 + transitivePeerDependencies: + - supports-color + '@embroider/shared-internals@2.8.1': dependencies: babel-import-util: 2.1.1 @@ -6504,8 +6646,32 @@ snapshots: dependencies: '@glimmer/interfaces': 0.94.6 + '@glint/ember-tsc@1.1.1(typescript@5.7.2)': + dependencies: + '@glimmer/syntax': 0.95.0 + '@glint/template': 1.7.4 + '@volar/kit': 2.4.28(typescript@5.7.2) + '@volar/language-core': 2.4.28 + '@volar/language-server': 2.4.28 + '@volar/language-service': 2.4.28 + '@volar/source-map': 2.4.28 + '@volar/test-utils': 2.4.28 + '@volar/typescript': 2.4.28 + content-tag: 3.1.3 + silent-error: 1.1.1 + typescript: 5.7.2 + volar-service-html: 0.0.70(@volar/language-service@2.4.28) + volar-service-typescript: 0.0.70(@volar/language-service@2.4.28) + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + transitivePeerDependencies: + - supports-color + '@glint/template@1.5.0': {} + '@glint/template@1.7.4': {} + '@handlebars/parser@2.0.0': {} '@handlebars/parser@2.2.1': {} @@ -7133,6 +7299,55 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@volar/kit@2.4.28(typescript@5.7.2)': + dependencies: + '@volar/language-service': 2.4.28 + '@volar/typescript': 2.4.28 + typesafe-path: 0.2.2 + typescript: 5.7.2 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/language-server@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + '@volar/language-service': 2.4.28 + '@volar/typescript': 2.4.28 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/language-service@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/source-map@2.4.28': {} + + '@volar/test-utils@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + '@volar/language-server': 2.4.28 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vscode/l10n@0.0.18': {} + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -7908,6 +8123,8 @@ snapshots: content-tag@2.0.3: {} + content-tag@3.1.3: {} + convert-source-map@2.0.0: {} core-js-compat@3.40.0: @@ -7943,7 +8160,7 @@ snapshots: css-tree@3.1.0: dependencies: mdn-data: 2.12.2 - source-map-js: 1.0.2 + source-map-js: 1.2.1 cssesc@3.0.0: {} @@ -8039,7 +8256,7 @@ snapshots: dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.1 dunder-proto@1.0.1: dependencies: @@ -8103,6 +8320,49 @@ snapshots: - supports-color - webpack + ember-auto-import@2.10.0(@glint/template@1.7.4)(webpack@5.94.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.10(@glint/template@1.7.4) + '@embroider/shared-internals': 2.8.1 + babel-loader: 8.4.1(@babel/core@7.26.0)(webpack@5.94.0) + babel-plugin-ember-modules-api-polyfill: 3.5.0 + babel-plugin-ember-template-compilation: 2.3.0 + babel-plugin-htmlbars-inline-precompile: 5.3.1 + babel-plugin-syntax-dynamic-import: 6.18.0 + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + css-loader: 5.2.7(webpack@5.94.0) + debug: 4.4.3 + fs-extra: 10.1.0 + fs-tree-diff: 2.0.1 + handlebars: 4.7.8 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.21 + mini-css-extract-plugin: 2.9.2(webpack@5.94.0) + minimatch: 3.1.2 + parse5: 6.0.1 + pkg-entry-points: 1.1.1 + resolve: 1.22.10 + resolve-package-path: 4.0.3 + semver: 7.6.3 + style-loader: 2.0.0(webpack@5.94.0) + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 + transitivePeerDependencies: + - '@glint/template' + - supports-color + - webpack + ember-cli-babel-plugin-helpers@1.1.1: {} ember-cli-babel@7.26.11: @@ -8303,7 +8563,7 @@ snapshots: - supports-color - webpack - ember-source@6.1.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.94.0): + ember-source@6.1.0(@glimmer/component@2.0.0)(@glint/template@1.7.4)(rsvp@4.8.5)(webpack@5.94.0): dependencies: '@babel/core': 7.26.0 '@ember/edition-utils': 1.2.0 @@ -8332,7 +8592,7 @@ snapshots: broccoli-funnel: 3.0.8 broccoli-merge-trees: 4.2.0 chalk: 4.1.2 - ember-auto-import: 2.10.0(@glint/template@1.5.0)(webpack@5.94.0) + ember-auto-import: 2.10.0(@glint/template@1.7.4)(webpack@5.94.0) ember-cli-babel: 8.2.0(@babel/core@7.26.0) ember-cli-get-component-path-option: 1.0.0 ember-cli-is-package-missing: 1.0.0 @@ -9967,7 +10227,7 @@ snapshots: lower-case@2.0.2: dependencies: - tslib: 2.6.2 + tslib: 2.8.1 lru-cache@10.4.3: {} @@ -10155,7 +10415,7 @@ snapshots: no-case@3.0.4: dependencies: lower-case: 2.0.2 - tslib: 2.6.2 + tslib: 2.8.1 node-releases@2.0.19: {} @@ -10357,6 +10617,8 @@ snapshots: parse5@6.0.1: {} + path-browserify@1.0.1: {} + path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -10638,6 +10900,8 @@ snapshots: transitivePeerDependencies: - supports-color + request-light@0.7.0: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -10919,7 +11183,7 @@ snapshots: snake-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.8.1 socks-proxy-agent@6.2.1: dependencies: @@ -10934,8 +11198,6 @@ snapshots: ip: 2.0.0 smart-buffer: 4.2.0 - source-map-js@1.0.2: {} - source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -11203,8 +11465,6 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.6.2: {} - tslib@2.8.1: {} type-check@0.4.0: @@ -11309,6 +11569,12 @@ snapshots: possible-typed-array-names: 1.0.0 reflect.getprototypeof: 1.0.10 + typesafe-path@0.2.2: {} + + typescript-auto-import-cache@0.3.6: + dependencies: + semver: 7.6.3 + typescript-eslint@8.19.1(eslint@8.57.1)(typescript@5.7.2): dependencies: '@typescript-eslint/eslint-plugin': 8.19.1(@typescript-eslint/parser@8.19.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) @@ -11475,6 +11741,51 @@ snapshots: - supports-color - terser + volar-service-html@0.0.70(@volar/language-service@2.4.28): + dependencies: + vscode-html-languageservice: 5.6.2 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + + volar-service-typescript@0.0.70(@volar/language-service@2.4.28): + dependencies: + path-browserify: 1.0.1 + semver: 7.6.3 + typescript-auto-import-cache: 0.3.6 + vscode-languageserver-textdocument: 1.0.12 + vscode-nls: 5.2.0 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + + vscode-html-languageservice@5.6.2: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@3.1.0: {} + walk-sync@0.3.4: dependencies: ensure-posix-path: 1.1.1 diff --git a/src/parser/glint-utils.js b/src/parser/glint-utils.js new file mode 100644 index 0000000..17cf754 --- /dev/null +++ b/src/parser/glint-utils.js @@ -0,0 +1,57 @@ +let glintAvailable = false; +let rewriteModule, ConfigLoader; + +try { + ({ rewriteModule } = require('@glint/ember-tsc/transform')); + ({ ConfigLoader } = require('@glint/ember-tsc')); + glintAvailable = true; +} catch { + // @glint/ember-tsc not installed or Node too old for ESM require() +} + +const configLoader = glintAvailable ? new ConfigLoader() : null; + +/** + * @returns {boolean} + */ +function isGlintAvailable() { + return glintAvailable; +} + +/** + * Loads and caches GlintConfig for the project containing filePath. + * Returns null if @glint/ember-tsc is not available or no glint + * environment is configured in the project's tsconfig. + * @param {string} filePath + * @returns {import('@glint/ember-tsc').GlintConfig | null} + */ +function getGlintConfig(filePath) { + if (!configLoader) return null; + try { + const config = configLoader.configForFile(filePath); + if (!config || config.environment.names.length === 0) return null; + return config; + } catch { + return null; + } +} + +/** + * Rewrites a .gts/.gjs module using Glint's template-to-TypeScript transform. + * Returns TransformedModule or null if no templates found / transform not needed. + * @param {string} code - file contents + * @param {string} filePath - absolute file path + * @param {*} ts - TypeScript instance + * @param {import('@glint/ember-tsc').GlintConfig} config - Glint config + * @returns {{ transformedContents: string } | null} + */ +function glintRewriteModule(code, filePath, ts, config) { + if (!rewriteModule) return null; + return rewriteModule( + ts, + { script: { filename: filePath, contents: code } }, + config.environment + ); +} + +module.exports = { isGlintAvailable, getGlintConfig, glintRewriteModule }; diff --git a/src/parser/ts-patch.js b/src/parser/ts-patch.js index efc6fbb..e790307 100644 --- a/src/parser/ts-patch.js +++ b/src/parser/ts-patch.js @@ -1,6 +1,7 @@ const fs = require('node:fs'); const { transformForLint } = require('./transforms'); const { replaceRange } = require('./transforms'); +const { isGlintAvailable, getGlintConfig, glintRewriteModule } = require('./glint-utils'); let patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser, isPatched, allowGjs; @@ -50,11 +51,28 @@ try { content = fs.readFileSync(fileName).toString(); } if (fileName.endsWith('.gts') || (allowGjs && fileName.endsWith('.gjs'))) { - try { - content = transformForLint(content).output; - } catch (e) { - console.error('failed to transformForLint for gts/gjs processing'); - console.error(e); + let transformed = false; + if (isGlintAvailable()) { + try { + const config = getGlintConfig(fileName); + if (config) { + const result = glintRewriteModule(content, fileName, ts, config); + if (result) { + content = result.transformedContents; + transformed = true; + } + } + } catch (e) { + // Glint transform failed, fall through to transformForLint + } + } + if (!transformed) { + try { + content = transformForLint(content).output; + } catch (e) { + console.error('failed to transformForLint for gts/gjs processing'); + console.error(e); + } } } if ( diff --git a/test-projects/gts-glint/.eslintignore b/test-projects/gts-glint/.eslintignore new file mode 100644 index 0000000..cc1b7f1 --- /dev/null +++ b/test-projects/gts-glint/.eslintignore @@ -0,0 +1 @@ +tsconfig.tsbuildinfo diff --git a/test-projects/gts-glint/.eslintrc.cjs b/test-projects/gts-glint/.eslintrc.cjs new file mode 100644 index 0000000..955947a --- /dev/null +++ b/test-projects/gts-glint/.eslintrc.cjs @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = { + root: true, + rules: { + 'no-unused-vars': ['error'], + }, + overrides: [ + { + files: ['**/*.{js,ts}'], + plugins: ['ember'], + parser: 'ember-eslint-parser', + extends: ['eslint:recommended', 'plugin:ember/recommended'], + }, + { + files: ['**/*.gts'], + parser: 'ember-eslint-parser', + plugins: ['ember'], + extends: ['eslint:recommended', 'plugin:ember/recommended', 'plugin:ember/recommended-gts'], + }, + ], +}; diff --git a/test-projects/gts-glint/index.d.ts b/test-projects/gts-glint/index.d.ts new file mode 100644 index 0000000..8766a46 --- /dev/null +++ b/test-projects/gts-glint/index.d.ts @@ -0,0 +1 @@ +import "ember-source/types/stable" diff --git a/test-projects/gts-glint/package.json b/test-projects/gts-glint/package.json new file mode 100644 index 0000000..64e00f5 --- /dev/null +++ b/test-projects/gts-glint/package.json @@ -0,0 +1,21 @@ +{ + "name": "@test-project/gts-glint", + "private": true, + "scripts": { + "test:check": "eslint src --max-warnings=0" + }, + "devDependencies": { + "@ember/test-waiters": "^3.1.0", + "@glimmer/tracking": "^1.1.2", + "@glimmer/component": "^1.1.2", + "@glint/ember-tsc": "^1.1.0", + "@glint/template": "^1.3.0", + "@typescript-eslint/eslint-plugin": "^8.46.4", + "@typescript-eslint/parser": "^8.46.4", + "ember-eslint-parser": "workspace:^", + "ember-source": "^5.6.0", + "eslint": "^8.0.1", + "eslint-plugin-ember": "^12.0.0", + "typescript": "^5.3.3" + } +} diff --git a/test-projects/gts-glint/src/consumer.ts b/test-projects/gts-glint/src/consumer.ts new file mode 100644 index 0000000..e4f5c92 --- /dev/null +++ b/test-projects/gts-glint/src/consumer.ts @@ -0,0 +1,5 @@ +import { greet } from './greeter'; + +const message: string = greet('world'); + +export { message }; diff --git a/test-projects/gts-glint/src/greeter.gts b/test-projects/gts-glint/src/greeter.gts new file mode 100644 index 0000000..cfee513 --- /dev/null +++ b/test-projects/gts-glint/src/greeter.gts @@ -0,0 +1,19 @@ +import Component from '@glimmer/component'; + +interface GreeterArgs { + name: string; +} + +export function greet(name: string): string { + return `Hello, ${name}`; +} + +export default class Greeter extends Component<{ Args: GreeterArgs }> { + get greeting(): string { + return greet(this.args.name); + } + + +} diff --git a/test-projects/gts-glint/tsconfig.json b/test-projects/gts-glint/tsconfig.json new file mode 100644 index 0000000..9fb615d --- /dev/null +++ b/test-projects/gts-glint/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": ["ES2018", "DOM"], + "module": "commonjs", + "experimentalDecorators": true, + "rootDir": ".", + "allowJs": true, + "strictNullChecks": true + }, + "glint": { + "environment": "ember-template-imports" + }, + "include": [ + "**/*.ts", + "**/*.gts", + "index.d.ts" + ] +} From b118984714deb2f6de6a844d1bb692245d34084c Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 27 Feb 2026 13:25:57 -0800 Subject: [PATCH 2/3] Integrate Glint for current-file TS transform (Phase 2) Add a Glint-based code path in parseForESLint that uses rewriteModule() for the file being linted, giving the TS type checker full understanding of template semantics. Positions are remapped from Glint's transformed-space back to original-space using correlatedSpans, then Glimmer AST is spliced in. - Create src/parser/remap.js with position remapping utilities - Add buildTemplateInfoFromGlint to glint-utils.js - Refactor preprocessGlimmerTemplates; add char-offset variant - Add matchByRangeOnly option to convertAst for Glint node types - Export ts instance from ts-patch.js - Add parseWithGlint orchestration in gjs-gts-parser.js Falls back to existing transformForLint path when Glint is unavailable or rewriteModule returns null. Co-Authored-By: Claude Opus 4.6 --- src/parser/gjs-gts-parser.js | 107 ++++++++++++++++- src/parser/glint-utils.js | 31 ++++- src/parser/remap.js | 216 +++++++++++++++++++++++++++++++++++ src/parser/transforms.js | 68 ++++++++--- src/parser/ts-patch.js | 5 +- 5 files changed, 404 insertions(+), 23 deletions(-) create mode 100644 src/parser/remap.js diff --git a/src/parser/gjs-gts-parser.js b/src/parser/gjs-gts-parser.js index cdc16e2..7023c87 100644 --- a/src/parser/gjs-gts-parser.js +++ b/src/parser/gjs-gts-parser.js @@ -6,8 +6,16 @@ const { replaceExtensions, syncMtsGtsSourceFiles, typescriptParser, + ts, } = require('./ts-patch'); -const { transformForLint, preprocessGlimmerTemplates, convertAst } = require('./transforms'); +const { + transformForLint, + preprocessGlimmerTemplates, + preprocessGlimmerTemplatesFromCharOffsets, + convertAst, +} = require('./transforms'); +const { isGlintAvailable, getGlintConfig, glintRewriteModule, buildTemplateInfoFromGlint } = require('./glint-utils'); +const { remapAstPositions, remapTokens } = require('./remap'); /** * implements https://eslint.org/docs/latest/extend/custom-parsers @@ -128,6 +136,83 @@ function getAllowJs(options) { return false; } +/** + * Parse using Glint's transform for full type-aware template support. + * Glint transforms templates into __glintDSL__ calls that TS understands, + * then we remap AST positions back to original source and splice in Glimmer AST. + */ +function parseWithGlint(code, options, transformedModule, allowGjsWasSet, actualAllowGjs, allowGjs) { + const filePath = options.filePath; + + // Get transformed TS code and replace .gts→.mts imports + let tsCode = transformedModule.transformedContents; + if (options.project || options.projectService) { + tsCode = replaceExtensions(tsCode); + } + + // Parse the transformed code with TS parser (positions in transformed-space) + const result = typescriptParser.parseForESLint(tsCode, { + ...options, + ranges: true, + extraFileExtensions: ['.gts', '.gjs'], + filePath, + }); + + // Build template infos from Glint's correlatedSpans + const glintTemplateInfos = buildTemplateInfoFromGlint(transformedModule, filePath); + + // Always remap positions even if no templates — Glint may have changed code length + // for non-template spans (e.g., directive placeholders) + const { templateSpans } = remapAstPositions( + result.ast, + result.visitorKeys, + transformedModule.correlatedSpans, + code + ); + + // Remap tokens + result.ast.tokens = remapTokens( + result.ast.tokens, + transformedModule.correlatedSpans, + templateSpans, + code + ); + + if (!glintTemplateInfos.length) { + return result; + } + + // Preprocess Glimmer templates (parse to Glimmer AST with correct positions) + const preprocessedResult = preprocessGlimmerTemplatesFromCharOffsets(glintTemplateInfos, code); + preprocessedResult.code = code; + const { templateVisitorKeys } = preprocessedResult; + const visitorKeys = { ...result.visitorKeys, ...templateVisitorKeys }; + result.isTypescript = true; + + // Splice Glimmer AST into the remapped TS AST (matchByRangeOnly because + // Glint produces different node types than transformForLint) + convertAst(result, preprocessedResult, visitorKeys, { matchByRangeOnly: true }); + + if (result.services?.program) { + const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs; + if ( + !allowGjsWasSet && + programAllowJs !== undefined && + actualAllowGjs !== undefined && + actualAllowGjs !== programAllowJs + ) { + // eslint-disable-next-line no-console + console.warn( + '[ember-eslint-parser] allowJs does not match the actual program. Consider setting allowGjs explicitly.\n' + + ` Current: ${allowGjs}, Program: ${programAllowJs}` + ); + } + syncMtsGtsSourceFiles(result.services.program); + } + + return { ...result, visitorKeys }; +} + /** * @type {import('eslint').ParserModule} */ @@ -146,6 +231,26 @@ module.exports = { ({ allowGjs: actualAllowGjs } = patchTs({ allowGjs })); } registerParsedFile(options.filePath); + + // Try Glint path for .gts/.gjs files when Glint is available + const isGts = options.filePath.endsWith('.gts'); + const isGjs = options.filePath.endsWith('.gjs'); + if ((isGts || isGjs) && isGlintAvailable() && ts && typescriptParser) { + try { + const glintConfig = getGlintConfig(options.filePath); + if (glintConfig) { + const glintTransform = glintRewriteModule(code, options.filePath, ts, glintConfig); + if (glintTransform) { + return parseWithGlint(code, options, glintTransform, allowGjsWasSet, actualAllowGjs, allowGjs); + } + } + } catch (e) { + // eslint-disable-next-line no-console + console.warn('[ember-eslint-parser] Glint path failed, falling back:', e.message); + } + } + + // Existing (non-Glint) path let jsCode = code; const info = transformForLint(code, options.filePath); jsCode = info.output; diff --git a/src/parser/glint-utils.js b/src/parser/glint-utils.js index 17cf754..0a7d93d 100644 --- a/src/parser/glint-utils.js +++ b/src/parser/glint-utils.js @@ -54,4 +54,33 @@ function glintRewriteModule(code, filePath, ts, config) { ); } -module.exports = { isGlintAvailable, getGlintConfig, glintRewriteModule }; +/** + * Build template info objects from a Glint TransformedModule's correlatedSpans. + * Returns template ranges in character (UTF-16) offsets suitable for + * preprocessGlimmerTemplatesFromCharOffsets. + * + * @param {object} transformedModule - Glint TransformedModule + * @param {string} originalFileName - original file path + * @returns {Array<{ range: [number, number], contentRange: [number, number] }>} + */ +function buildTemplateInfoFromGlint(transformedModule, originalFileName) { + const result = []; + for (const span of transformedModule.correlatedSpans) { + if (!span.glimmerAstMapping) continue; + + const fullStart = span.originalStart; + const fullEnd = span.originalStart + span.originalLength; + + // Use findTemplateAtOriginalOffset to get content bounds (excludes