From 0ad9f56a0859d99499a71de6b8ae71f2a67309b7 Mon Sep 17 00:00:00 2001 From: machty Date: Tue, 9 Jul 2024 09:01:20 -0400 Subject: [PATCH 01/18] rm hbs from gts language plugin, see what breaks wip --- packages/core/__tests__/cli/check.test.ts | 7 +++---- .../language-server/completions.test.ts | 18 +++++++----------- .../src/transform/template/rewrite-module.ts | 6 ++++++ packages/core/src/volar/gts-language-plugin.ts | 16 ++++++++-------- .../core/src/volar/handlebars-virtual-code.ts | 6 +++++- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/core/__tests__/cli/check.test.ts b/packages/core/__tests__/cli/check.test.ts index ece8ac78e..a6fe77ec4 100644 --- a/packages/core/__tests__/cli/check.test.ts +++ b/packages/core/__tests__/cli/check.test.ts @@ -176,7 +176,7 @@ describe('CLI: single-pass typechecking', () => { `); }); - test.skip('reports diagnostics for a companion template type error', async () => { + test.only('reports diagnostics for a companion template type error', async () => { project.setGlintConfig({ environment: 'ember-loose' }); let script = stripIndent` @@ -200,9 +200,8 @@ describe('CLI: single-pass typechecking', () => { let checkResult = await project.check({ reject: false }); - expect(checkResult.exitCode).toBe(1); - expect(checkResult.stdout).toEqual(''); - expect(stripAnsi(checkResult.stderr)).toMatchInlineSnapshot(` + expect(checkResult.exitCode).not.toBe(0); + expect(stripAnsi(checkResult.stdout)).toMatchInlineSnapshot(` "my-component.hbs:1:22 - error TS2551: Property 'targett' does not exist on type 'MyComponent'. Did you mean 'target'? 1 {{@message}}, {{this.targett}} diff --git a/packages/core/__tests__/language-server/completions.test.ts b/packages/core/__tests__/language-server/completions.test.ts index bc45b39ad..e4beee0a3 100644 --- a/packages/core/__tests__/language-server/completions.test.ts +++ b/packages/core/__tests__/language-server/completions.test.ts @@ -14,21 +14,19 @@ describe('Language Server: Completions', () => { await project.destroy(); }); - test.skip('querying a standalone template', async () => { + test('querying a standalone template', async () => { project.setGlintConfig({ environment: 'ember-loose' }); project.write('index.hbs', ''); let server = await project.startLanguageServer(); - let completions = server.getCompletions(project.fileURI('index.hbs'), { - line: 0, - character: 6, - }); + const { uri } = await server.openTextDocument(project.filePath('index.hbs'), 'handlebars'); + let completions = await server.sendCompletionRequest(uri, Position.create(0, 6)); - let completion = completions?.find((item) => item.label === 'LinkTo'); + let completion = completions?.items.find((item) => item.label === 'LinkTo'); expect(completion?.kind).toEqual(CompletionItemKind.Field); - let details = server.getCompletionDetails(completion!); + let details = await server.sendCompletionResolveRequest(completion!); expect(details.detail).toEqual('(property) Globals.LinkTo: LinkToComponent'); }); @@ -65,10 +63,8 @@ describe('Language Server: Completions', () => { project.write('index.hbs', code); let server = await project.startLanguageServer(); - let completions = server.getCompletions(project.fileURI('index.hbs'), { - line: 0, - character: 4, - }); + const { uri } = await server.openTextDocument(project.filePath('index.hbs'), 'handlebars'); + let completions = await server.sendCompletionRequest(uri, Position.create(0, 4)); // Ensure we don't spew all ~900 completions available at the top level // in module scope in a JS/TS file. diff --git a/packages/core/src/transform/template/rewrite-module.ts b/packages/core/src/transform/template/rewrite-module.ts index b3488083f..3afc02954 100644 --- a/packages/core/src/transform/template/rewrite-module.ts +++ b/packages/core/src/transform/template/rewrite-module.ts @@ -106,6 +106,12 @@ function calculateCorrelatedSpans( ts.transform(ast, [ (context) => function visit(node: T): T { + // Here we look for ```hbs``` tagged template expressions, originally introduced + // in the now-removed GlimmerX environment. We can consider getting rid of this, but + // then again there are still some use cases in the wild (e.g. Glimmer Next / GXT) + // where have tagged templates closing over outer scope is desirable: + // https://github.com/lifeart/glimmer-next/tree/master/glint-environment-gxt + // https://discord.com/channels/480462759797063690/717767358743183412/1259061848632721480 if (ts.isTaggedTemplateExpression(node)) { let meta = emitMetadata.get(node); let result = calculateTaggedTemplateSpans(ts, node, meta, script, environment); diff --git a/packages/core/src/volar/gts-language-plugin.ts b/packages/core/src/volar/gts-language-plugin.ts index 87de414d6..30f69ff99 100644 --- a/packages/core/src/volar/gts-language-plugin.ts +++ b/packages/core/src/volar/gts-language-plugin.ts @@ -11,7 +11,8 @@ import { URI } from 'vscode-uri'; export type TS = typeof ts; /** - * Create a [Volar](https://volarjs.dev) language module to support GTS. + * Create a [Volar](https://volarjs.dev) language module to support .gts/.gjs files + * (the `ember-template-imports` environment) */ export function createGtsLanguagePlugin( glintConfig: GlintConfig, @@ -57,9 +58,9 @@ export function createGtsLanguagePlugin( typescript: { extraFileExtensions: [ - { extension: 'gts', isMixedContent: true, scriptKind: 7 }, - { extension: 'gjs', isMixedContent: true, scriptKind: 7 }, - { extension: 'hbs', isMixedContent: true, scriptKind: 7 }, + { extension: 'gts', isMixedContent: true, scriptKind: 7 satisfies ts.ScriptKind.Deferred }, + { extension: 'gjs', isMixedContent: true, scriptKind: 7 satisfies ts.ScriptKind.Deferred }, + { extension: 'hbs', isMixedContent: true, scriptKind: 7 satisfies ts.ScriptKind.Deferred }, ], // Allow extension-less imports, e.g. `import Foo from './Foo`. @@ -82,21 +83,20 @@ export function createGtsLanguagePlugin( return { code: transformedCode, extension: '.ts', - scriptKind: 3, // TS + scriptKind: 3 satisfies ts.ScriptKind.TS, }; case 'glimmer-js': return { - // The first embeddedCode is always the TS Intermediate Representation code code: transformedCode, extension: '.js', - scriptKind: 1, // JS + scriptKind: 1 satisfies ts.ScriptKind.JS, }; case 'handlebars': // TODO: companion file might be .js? Not sure if this is right return { code: transformedCode, extension: '.ts', - scriptKind: 3, // TS + scriptKind: 3 satisfies ts.ScriptKind.TS, }; default: throw new Error(`getScript: Unexpected languageId: ${rootVirtualCode.languageId}`); diff --git a/packages/core/src/volar/handlebars-virtual-code.ts b/packages/core/src/volar/handlebars-virtual-code.ts index 15b526a9c..543ef422d 100644 --- a/packages/core/src/volar/handlebars-virtual-code.ts +++ b/packages/core/src/volar/handlebars-virtual-code.ts @@ -66,7 +66,11 @@ export class VirtualHandlebarsCode implements VirtualCode { // it doesn't have a companion script elsewhere. // We default to just `export {}` to reassure TypeScript that this is definitely a module // TODO: this `export {}` is falsely mapping (see in Volar Labs), not sure what impact / solution is. - let script = { filename: 'disregard.ts', contents: 'export {}' }; + + // Here we are assembling the args to pass into rewriteModule, which wants both the .ts script + // and the template file. For .gts template is undefined but here we need to pass in the contents. + // Let's see how rewriteModule attempts to transform this shit. + let script = { filename: 'disregard.ts', contents: 'export {}; let a = 123;' }; let template = { filename: 'disregard.hbs', contents, From 69979264cbe67a76e8a651109253943c674d3bbd Mon Sep 17 00:00:00 2001 From: machty Date: Tue, 9 Jul 2024 10:59:26 -0400 Subject: [PATCH 02/18] wip --- packages/core/__tests__/cli/check.test.ts | 2 +- .../src/transform/template/inlining/companion-file.ts | 8 ++++++++ .../core/src/transform/template/map-template-contents.ts | 3 ++- packages/core/src/volar/handlebars-virtual-code.ts | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/core/__tests__/cli/check.test.ts b/packages/core/__tests__/cli/check.test.ts index a6fe77ec4..ba135128c 100644 --- a/packages/core/__tests__/cli/check.test.ts +++ b/packages/core/__tests__/cli/check.test.ts @@ -176,7 +176,7 @@ describe('CLI: single-pass typechecking', () => { `); }); - test.only('reports diagnostics for a companion template type error', async () => { + test('reports diagnostics for a companion template type error', async () => { project.setGlintConfig({ environment: 'ember-loose' }); let script = stripIndent` diff --git a/packages/core/src/transform/template/inlining/companion-file.ts b/packages/core/src/transform/template/inlining/companion-file.ts index 4417b84e1..6ba43c182 100644 --- a/packages/core/src/transform/template/inlining/companion-file.ts +++ b/packages/core/src/transform/template/inlining/companion-file.ts @@ -49,6 +49,8 @@ export function calculateCompanionTemplateSpans( suffix: '}\n', }); } else { + // TODO: when does this get called? + throw new Error("ALEX not class like"); let backingValue: string | undefined; if (targetNode) { let moduleName = path.basename(script.filename, path.extname(script.filename)); @@ -143,6 +145,9 @@ export function calculateCompanionTemplateSpans( } } +/** + * Find and return the TS AST node which can serve as a proper insertion point + */ function findCompanionTemplateTarget( ts: TSLib, sourceFile: ts.SourceFile, @@ -155,6 +160,7 @@ function findCompanionTemplateTarget( mods?.some((mod) => mod.kind === ts.SyntaxKind.DefaultKeyword) && mods.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword) ) { + // We've found a `export default class` statement; return it. return statement; } @@ -164,6 +170,8 @@ function findCompanionTemplateTarget( } } + // We didn't find a default export, but maybe there is a named export that + // matches one of the class statements we found above. for (let statement of sourceFile.statements) { if (ts.isExportAssignment(statement) && !statement.isExportEquals) { if (ts.isIdentifier(statement.expression) && statement.expression.text in classes) { diff --git a/packages/core/src/transform/template/map-template-contents.ts b/packages/core/src/transform/template/map-template-contents.ts index 10aebdb85..da5bef578 100644 --- a/packages/core/src/transform/template/map-template-contents.ts +++ b/packages/core/src/transform/template/map-template-contents.ts @@ -121,7 +121,8 @@ export type MapTemplateContentsOptions = { }; /** - * Given the text of an embedded template, invokes the given callback + * Given the text of a handlebars template (either standalone .hbs file, or the contents + * of an embedded `` within a .gts file), invokes the given callback * with a set of tools to emit mapped contents corresponding to * that template, tracking the text emitted in order to provide * a mapping of ranges in the input to ranges in the output. diff --git a/packages/core/src/volar/handlebars-virtual-code.ts b/packages/core/src/volar/handlebars-virtual-code.ts index 543ef422d..9bf9fdee2 100644 --- a/packages/core/src/volar/handlebars-virtual-code.ts +++ b/packages/core/src/volar/handlebars-virtual-code.ts @@ -70,7 +70,7 @@ export class VirtualHandlebarsCode implements VirtualCode { // Here we are assembling the args to pass into rewriteModule, which wants both the .ts script // and the template file. For .gts template is undefined but here we need to pass in the contents. // Let's see how rewriteModule attempts to transform this shit. - let script = { filename: 'disregard.ts', contents: 'export {}; let a = 123;' }; + let script = { filename: 'disregard.ts', contents: 'export default class MyComponent {}' }; let template = { filename: 'disregard.hbs', contents, From 1f97870d5b99456fefa1481eaac2f6dff5222b35 Mon Sep 17 00:00:00 2001 From: machty Date: Tue, 9 Jul 2024 13:09:17 -0400 Subject: [PATCH 03/18] wip loose mode virutal code --- .../core/src/volar/gts-language-plugin.ts | 45 ++++++++---- packages/core/src/volar/gts-virtual-code.ts | 3 +- ...e-backing-component-class-virtual-code.ts} | 72 ++++++++++--------- 3 files changed, 73 insertions(+), 47 deletions(-) rename packages/core/src/volar/{handlebars-virtual-code.ts => loose-mode-backing-component-class-virtual-code.ts} (61%) diff --git a/packages/core/src/volar/gts-language-plugin.ts b/packages/core/src/volar/gts-language-plugin.ts index 30f69ff99..7a038a18b 100644 --- a/packages/core/src/volar/gts-language-plugin.ts +++ b/packages/core/src/volar/gts-language-plugin.ts @@ -1,13 +1,9 @@ -// import remarkMdx from 'remark-mdx' -// import remarkParse from 'remark-parse' -// import {unified} from 'unified' import { LanguagePlugin } from '@volar/language-core'; import { VirtualGtsCode } from './gts-virtual-code.js'; import type ts from 'typescript'; -import { GlintConfig, loadConfig } from '../index.js'; -import { assert } from '../transform/util.js'; -import { VirtualHandlebarsCode } from './handlebars-virtual-code.js'; +import { GlintConfig } from '../index.js'; import { URI } from 'vscode-uri'; +import { LooseModeBackingComponentClassVirtualCode } from './loose-mode-backing-component-class-virtual-code.js'; export type TS = typeof ts; /** @@ -41,9 +37,24 @@ export function createGtsLanguagePlugin( }, createVirtualCode(uri, languageId, snapshot) { - // TODO: won't we need to point the TS component code to the same thing? - if (languageId === 'handlebars') { - return new VirtualHandlebarsCode(glintConfig, snapshot); + const scriptId = String(uri); + + // See: https://github.com/JetBrains/intellij-plugins/blob/11a9149e20f4d4ba2c1600da9f2b81ff88bd7c97/Angular/src/angular-service/src/index.ts#L31 + if ( + languageId === 'typescript' && + !scriptId.endsWith('.d.ts') && + scriptId.indexOf('/node_modules/') < 0 && + scriptId.indexOf('components/') >= 0 // match anything in the components directory + ) { + // let virtualCode = ngTcbBlocks.get(scriptId); + // if (!virtualCode) { + // virtualCode = new AngularVirtualCode(scriptId, ctx, ts.sys.useCaseSensitiveFileNames); + // ngTcbBlocks.set(scriptId, virtualCode); + // } + // return virtualCode.sourceFileUpdated(snapshot); + + // Need a new VirtualCode LooseModeBackingComponentClassVirtualCode + return new LooseModeBackingComponentClassVirtualCode(glintConfig, snapshot); } if (languageId === 'glimmer-ts' || languageId === 'glimmer-js') { @@ -51,9 +62,19 @@ export function createGtsLanguagePlugin( } }, - updateVirtualCode(uri, virtualCode, snapshot) { - (virtualCode as VirtualGtsCode).update(snapshot); - return virtualCode; + // This is the default implementation; should be able to comment out + // updateVirtualCode(uri, virtualCode, snapshot) { + // (virtualCode as VirtualGtsCode | LooseModeBackingComponentClassVirtualCode).update(snapshot); + // return virtualCode; + // }, + + isAssociatedFileOnly(_scriptId: string | URI, languageId: string): boolean { + // `ember-loose` only + // + // Because we declare handlebars files to be associated with "root" .ts files, we + // need to mark them here as "associated file only" so that TS doesn't attempt + // to type-check them directly, but rather indirectly via the .ts file. + return languageId === 'handlebars'; }, typescript: { diff --git a/packages/core/src/volar/gts-virtual-code.ts b/packages/core/src/volar/gts-virtual-code.ts index 3592d79c6..d5fe15754 100644 --- a/packages/core/src/volar/gts-virtual-code.ts +++ b/packages/core/src/volar/gts-virtual-code.ts @@ -11,7 +11,8 @@ interface EmbeddedCodeWithDirectives extends VirtualCode { } /** - * A Volar virtual code that contains some additional metadata for MDX files. + * A Volar VirtualCode representing .gts/.gjs files, which includes 0+ embedded + * Handlebars templates within