Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/ember-tsc/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export function loadConfig(from: string): GlintConfig {
* and searching upwards. Returns `null` if no configuration is
* found.
*/
export function findConfig(from: string): GlintConfig | null {
return new ConfigLoader().configForDirectory(from);
export function findConfig(from: string, fallbackTs?: typeof TS): GlintConfig | null {
return new ConfigLoader(undefined, fallbackTs).configForDirectory(from);
}

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/ember-tsc/src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ type TypeScript = typeof TS;
export class ConfigLoader {
private configs = new Map<string, GlintConfig | null>();
private logInfo?: (message: string) => void;
private fallbackTypeScript?: TypeScript;

constructor(logInfo?: (message: string) => void) {
constructor(logInfo?: (message: string) => void, fallbackTypeScript?: TypeScript) {
this.logInfo = logInfo;
this.fallbackTypeScript = fallbackTypeScript;
}

private log(message: string): void {
Expand All @@ -38,7 +40,7 @@ export class ConfigLoader {
}

public configForDirectory(directory: string): GlintConfig | null {
let ts = findTypeScript(directory);
let ts = findTypeScript(directory) ?? this.fallbackTypeScript ?? null;
if (!ts) {
this.log(`No TypeScript installation found from ${directory}.`);
return null;
Expand Down
39 changes: 35 additions & 4 deletions packages/ember-tsc/src/volar/language-server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createRequire } from 'node:module';
import * as path from 'node:path';
import type TS from 'typescript';
import { createLanguage } from '@volar/language-core';
import type { LanguagePlugin, LanguageServer } from '@volar/language-server';
import { createLanguageServiceEnvironment } from '@volar/language-server/lib/project/simpleProject.js';
import { createConnection, createServer } from '@volar/language-server/node.js';
import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service';
import { createLanguageService, createUriMap, LanguageService } from '@volar/language-service';
import * as ts from 'typescript';
import { create as createHtmlSyntacticPlugin } from 'volar-service-html';
import { create as createTypeScriptSyntacticPlugin } from 'volar-service-typescript/lib/plugins/syntactic.js';
import { URI } from 'vscode-uri';
Expand All @@ -17,6 +18,36 @@ import type { ComponentMeta, TsPluginClient } from '../plugins/g-component-hover
import { create as createTemplateTagSymbolsPlugin } from '../plugins/g-template-tag-symbols.js';
import { createEmberLanguagePlugin } from './ember-language-plugin.js';

const require = createRequire(import.meta.url);

/**
* Resolve TypeScript with fallback to a path provided by the host (e.g., VS Code's built-in TypeScript).
* This allows the language server to work even when the user's project doesn't have `typescript`
* as a direct dependency (only `@glint/ember-tsc` which has it as a peerDep).
*/
function resolveTypeScript(): typeof import('typescript') {
// Try normal resolution (project's TypeScript or ember-tsc's peer dep)
try {
return require('typescript');
} catch {
// ignore
}

// Fall back to TypeScript path provided by the VS Code extension
const fallbackPath = process.env['GLINT_TYPESCRIPT_PATH'];
if (fallbackPath) {
try {
return require(fallbackPath);
} catch {
// ignore
}
}

throw new Error('TypeScript could not be resolved. Please install `typescript` as a dependency.');
}

const ts = resolveTypeScript();

const connection = createConnection();
const server = createServer(connection);
const tsserverRequestHandlers = new Map<number, (res: any) => void>();
Expand Down Expand Up @@ -78,12 +109,12 @@ connection.onInitialize((params) => {
if (uri.scheme === 'file') {
// Use tsserver to find the tsconfig governing this file.
const fileName = uri.fsPath.replace(/\\/g, '/');
const projectInfo = await sendTsServerRequest<ts.server.protocol.ProjectInfo>(
const projectInfo = await sendTsServerRequest<TS.server.protocol.ProjectInfo>(
'_glint:' + ts.server.protocol.CommandTypes.ProjectInfo,
{
file: fileName,
needFileNameList: false,
} satisfies ts.server.protocol.ProjectInfoRequestArgs,
} satisfies TS.server.protocol.ProjectInfoRequestArgs,
);
if (projectInfo) {
const { configFileName } = projectInfo;
Expand Down Expand Up @@ -152,7 +183,7 @@ connection.onInitialize((params) => {
!tsconfigFileName.startsWith('/dev/null') &&
tsconfigFileName.endsWith('.json');
if (isRealConfigFile) {
const configLoader = new ConfigLoader(logInfo);
const configLoader = new ConfigLoader(logInfo, ts);
glintConfig = configLoader.configForFile(tsconfigFileName);
}
if (!glintConfig) {
Expand Down
2 changes: 1 addition & 1 deletion packages/tsserver-plugin/src/typescript-server-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ const plugin = createLanguageServicePlugin(
logInfo(`Using ${resolved.source} ember-tsc from ${resolved.resolvedPath}.`);

const { findConfig, createDefaultConfig, createEmberLanguagePlugin } = emberTsc;
const glintConfig = findConfig(cwd) ?? createDefaultConfig(ts, cwd);
const glintConfig = findConfig(cwd, ts) ?? createDefaultConfig(ts, cwd);

const gtsLanguagePlugin = createEmberLanguagePlugin(glintConfig, {
clientId: 'tsserver-plugin',
Expand Down
43 changes: 41 additions & 2 deletions packages/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,19 +365,29 @@ function launch(

const serverPath = resolution.path;

// Provide VS Code's built-in TypeScript as a fallback for projects that
// have @glint/ember-tsc but not typescript as a direct dependency.
const builtinTsdkPath = resolveTsdkPath();
const serverEnv = {
...process.env,
...(builtinTsdkPath
? { GLINT_TYPESCRIPT_PATH: path.join(builtinTsdkPath, 'typescript.js') }
: {}),
};

const client = new lsp.LanguageClient(
'glint',
'Glint',
{
run: {
module: serverPath,
transport: lsp.TransportKind.ipc,
options: {},
options: { env: serverEnv },
},
debug: {
module: serverPath,
transport: lsp.TransportKind.ipc,
options: { execArgv: ['--nolazy', '--inspect=' + 6009] },
options: { execArgv: ['--nolazy', '--inspect=' + 6009], env: serverEnv },
},
},
{
Expand Down Expand Up @@ -487,6 +497,35 @@ function resolveWorkspaceEmberTscServerPath(resolutionDir: string): string | und
}
}

/**
* Resolve the path to the TypeScript SDK lib directory bundled with the editor.
* Supports VS Code and Eclipse Theia.
*/
function resolveTsdkPath(): string | undefined {
const vscodeTsdk = path.join(
vscode.env.appRoot,
'extensions',
'node_modules',
'typescript',
'lib',
);
if (fs.existsSync(vscodeTsdk)) {
return vscodeTsdk;
}

const tsExt = vscode.extensions.getExtension('vscode.typescript-language-features');
if (tsExt) {
// Eclipse Theia
// see: https://github.com/eclipse-theia/vscode-builtin-extensions/blob/65c70ec636bd879ef9529d0a2da36f4b99139c40/src/package-vsix.js#L71
const theiaTsdk = path.join(tsExt.extensionPath, 'deps', 'typescript', 'lib');
if (fs.existsSync(theiaTsdk)) {
return theiaTsdk;
}
}

return undefined;
}

function resolveBundledEmberTscServerPath(): string | undefined {
try {
const glintExtension = vscode.extensions.getExtension('typed-ember.glint2-vscode');
Expand Down
Loading