Skip to content

Commit 4d7883a

Browse files
committed
Support projects with no tsconfig or jsconfig
1 parent 16f4938 commit 4d7883a

18 files changed

Lines changed: 610 additions & 109 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ tsserver.log
77
dist/
88
.log/
99
.cursor
10+
.DS_Store

.vscode/launch.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@
2424
"${workspaceFolder}/packages/vscode/__fixtures__/ember-app"
2525
]
2626
},
27+
{
28+
// For this to work, make sure you're running `tsc --build --watch` at the root, AND
29+
// `pnpm bundle:watch` from within vscode directory.
30+
"name": "Debug Extension (TS Plugin, no config)",
31+
"type": "extensionHost",
32+
"request": "launch",
33+
// "preLaunchTask": "npm: build",
34+
"autoAttachChildProcesses": true,
35+
"runtimeExecutable": "${execPath}",
36+
"outFiles": [
37+
"${workspaceFolder}/**/*.js",
38+
"!**/node_modules/**"
39+
],
40+
"args": [
41+
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode",
42+
// Disable v1 vscode glint
43+
"--disable-extension",
44+
"typed-ember.glint-vscode",
45+
// comment to activate your local extensions
46+
"--disable-extensions",
47+
"${workspaceFolder}/test-packages/no-config-app"
48+
]
49+
},
2750
{
2851
"name": "Attach to TS Server",
2952
"type": "node",

packages/core/src/config/loader.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,20 @@ export class ConfigLoader {
4545
}
4646

4747
let configPath = findNearestConfigFile(ts, directory);
48+
49+
// If no tsconfig/jsconfig found, check if Glint-related dependencies are present
4850
if (!configPath) {
4951
this.log(`No tsconfig.json or jsconfig.json found from ${directory}.`);
50-
return null;
52+
if (!hasGlintRelatedDependencies(directory)) {
53+
return null;
54+
}
55+
// Found Glint-related dependencies but no config file
56+
// Return a minimal config with default ember-template-imports environment
57+
// to allow activation with basic behaviors enabled
58+
this.log(`Found Glint-related dependencies; using minimal default config.`);
59+
return new GlintConfig(ts, path.resolve(directory, 'tsconfig.json'), {
60+
environment: [],
61+
});
5162
}
5263

5364
let existing = this.configs.get(configPath);
@@ -139,6 +150,48 @@ function findNearestConfigFile(ts: TypeScript, searchFrom: string): string {
139150
return configCandidates[0];
140151
}
141152

153+
/**
154+
* Checks if @glint/template or ember-source is present in the package.json
155+
* of the given directory or any parent directory.
156+
*/
157+
function hasGlintRelatedDependencies(searchFrom: string): boolean {
158+
let currentDir = searchFrom;
159+
const root = path.parse(currentDir).root;
160+
161+
while (currentDir !== root) {
162+
const packageJsonPath = path.join(currentDir, 'package.json');
163+
164+
if (fs.existsSync(packageJsonPath)) {
165+
try {
166+
const content = fs.readFileSync(packageJsonPath, 'utf-8');
167+
const packageJson = JSON.parse(content);
168+
169+
const allDependencies = {
170+
...packageJson.dependencies,
171+
...packageJson.devDependencies,
172+
...packageJson.peerDependencies,
173+
...packageJson.optionalDependencies,
174+
};
175+
176+
if (allDependencies['@glint/template'] || allDependencies['ember-source']) {
177+
return true;
178+
}
179+
} catch {
180+
// Ignore parsing errors and continue searching
181+
}
182+
}
183+
184+
const parentDir = path.dirname(currentDir);
185+
if (parentDir === currentDir) {
186+
// Reached filesystem root
187+
break;
188+
}
189+
currentDir = parentDir;
190+
}
191+
192+
return false;
193+
}
194+
142195
function validateConfigInput(input: Record<string, unknown>): GlintConfigInput | null {
143196
if (!input['environment']) {
144197
input['environment'] = [];

packages/core/src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import { GlintConfig, loadConfig, findConfig } from './config/index.js';
2-
import { createEmberLanguagePlugin } from './volar/ember-language-plugin.js';
2+
import {
3+
createDynamicEmberLanguagePlugin,
4+
createEmberLanguagePlugin,
5+
} from './volar/ember-language-plugin.js';
36

47
import { VirtualGtsCode } from './volar/gts-virtual-code.js';
58
import { augmentDiagnostics } from './transform/diagnostics/augmentation.js';
69

7-
export { loadConfig, findConfig, createEmberLanguagePlugin, VirtualGtsCode, augmentDiagnostics };
10+
export {
11+
loadConfig,
12+
findConfig,
13+
createEmberLanguagePlugin,
14+
createDynamicEmberLanguagePlugin,
15+
VirtualGtsCode,
16+
augmentDiagnostics,
17+
};
818

919
export type { GlintConfig };

packages/core/src/volar/ember-language-plugin.ts

Lines changed: 116 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,71 @@
11
import { LanguagePlugin } from '@volar/language-core';
2+
import * as path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
24
import type ts from 'typescript';
35
import { URI } from 'vscode-uri';
46
import { GlintConfig } from '../index.js';
57
import { VirtualGtsCode } from './gts-virtual-code.js';
68
export type TS = typeof ts;
79

8-
/**
9-
* Create a [Volar](https://volarjs.dev) language plugin to support
10-
*
11-
* - .gts/.gjs files (the `ember-template-imports` environment)
12-
*/
13-
export function createEmberLanguagePlugin<T extends URI | string>(
14-
glintConfig: GlintConfig,
15-
{ clientId }: { clientId?: string } = {},
10+
type EmberLanguagePluginOptions = {
11+
clientId?: string;
12+
getCurrentDirectory?: () => string;
13+
allowJs?: boolean;
14+
throwOnUnexpectedLanguageId?: boolean;
15+
};
16+
17+
function getLanguageIdForFileName(fileName: string): 'glimmer-ts' | 'glimmer-js' | undefined {
18+
if (fileName.endsWith('.gts')) {
19+
return 'glimmer-ts';
20+
}
21+
if (fileName.endsWith('.gjs')) {
22+
return 'glimmer-js';
23+
}
24+
}
25+
26+
function isGlimmerLanguageId(
27+
languageId: string | undefined,
28+
): languageId is 'glimmer-ts' | 'glimmer-js' | 'typescript.glimmer' | 'javascript.glimmer' {
29+
return (
30+
languageId === 'glimmer-ts' ||
31+
languageId === 'glimmer-js' ||
32+
languageId === 'typescript.glimmer' ||
33+
languageId === 'javascript.glimmer'
34+
);
35+
}
36+
37+
function normalizeFileName(scriptIdStr: string, getCurrentDirectory?: () => string): string {
38+
let fileName = scriptIdStr;
39+
if (scriptIdStr.startsWith('file://')) {
40+
try {
41+
fileName = fileURLToPath(scriptIdStr);
42+
} catch {
43+
try {
44+
fileName = decodeURIComponent(new URL(scriptIdStr).pathname);
45+
} catch {
46+
fileName = scriptIdStr;
47+
}
48+
}
49+
}
50+
51+
if (!path.isAbsolute(fileName)) {
52+
const baseDir = getCurrentDirectory?.();
53+
if (baseDir) {
54+
fileName = path.resolve(baseDir, fileName);
55+
}
56+
}
57+
58+
return fileName;
59+
}
60+
61+
function createEmberLanguagePluginInternal<T extends URI | string>(
62+
getGlintConfig: (fileName: string) => GlintConfig | null,
63+
{
64+
clientId,
65+
getCurrentDirectory,
66+
allowJs = false,
67+
throwOnUnexpectedLanguageId = false,
68+
}: EmberLanguagePluginOptions = {},
1669
): LanguagePlugin<T> {
1770
return {
1871
/**
@@ -34,17 +87,21 @@ export function createEmberLanguagePlugin<T extends URI | string>(
3487
}
3588
},
3689

37-
createVirtualCode(scriptId: URI | string, languageId, snapshot, codegenContext) {
90+
createVirtualCode(scriptId: URI | string, languageId, snapshot) {
3891
const scriptIdStr = String(scriptId);
92+
const fileName = normalizeFileName(scriptIdStr, getCurrentDirectory);
93+
const inferredLanguageId = languageId ?? getLanguageIdForFileName(fileName);
3994

40-
if (
41-
languageId === 'glimmer-ts' ||
42-
languageId === 'glimmer-js' ||
43-
languageId === 'typescript.glimmer' ||
44-
languageId === 'javascript.glimmer'
45-
) {
46-
return new VirtualGtsCode(glintConfig, snapshot, languageId, clientId);
95+
if (!isGlimmerLanguageId(inferredLanguageId)) {
96+
return;
4797
}
98+
99+
const glintConfig = getGlintConfig(fileName);
100+
if (!glintConfig) {
101+
return;
102+
}
103+
104+
return new VirtualGtsCode(glintConfig, snapshot, inferredLanguageId, clientId);
48105
},
49106

50107
typescript: {
@@ -89,20 +146,52 @@ export function createEmberLanguagePlugin<T extends URI | string>(
89146
scriptKind: 1 satisfies ts.ScriptKind.JS,
90147
};
91148
default:
92-
throw new Error(`getScript: Unexpected languageId: ${rootVirtualCode.languageId}`);
149+
if (throwOnUnexpectedLanguageId) {
150+
throw new Error(`getScript: Unexpected languageId: ${rootVirtualCode.languageId}`);
151+
}
93152
}
94153
},
95154

96-
resolveLanguageServiceHost(host) {
97-
return {
98-
...host,
99-
getCompilationSettings: () => ({
100-
...host.getCompilationSettings(),
101-
// Always allow JS for type checking.
102-
allowJs: true,
103-
}),
104-
};
105-
},
155+
...(allowJs
156+
? {
157+
resolveLanguageServiceHost(host) {
158+
return {
159+
...host,
160+
getCompilationSettings: () => ({
161+
...host.getCompilationSettings(),
162+
// Always allow JS for type checking.
163+
allowJs: true,
164+
}),
165+
};
166+
},
167+
}
168+
: {}),
106169
},
107170
};
108171
}
172+
173+
/**
174+
* Create a [Volar](https://volarjs.dev) language plugin to support
175+
*
176+
* - .gts/.gjs files (the `ember-template-imports` environment)
177+
*/
178+
export function createEmberLanguagePlugin<T extends URI | string>(
179+
glintConfig: GlintConfig,
180+
{ clientId }: { clientId?: string } = {},
181+
): LanguagePlugin<T> {
182+
return createEmberLanguagePluginInternal(() => glintConfig, {
183+
clientId,
184+
allowJs: true,
185+
throwOnUnexpectedLanguageId: true,
186+
});
187+
}
188+
189+
export function createDynamicEmberLanguagePlugin<T extends URI | string>(
190+
findConfig: (from: string) => GlintConfig | null,
191+
{ clientId, getCurrentDirectory }: { clientId?: string; getCurrentDirectory: () => string },
192+
): LanguagePlugin<T> {
193+
return createEmberLanguagePluginInternal((fileName) => findConfig(path.dirname(fileName)), {
194+
clientId,
195+
getCurrentDirectory,
196+
});
197+
}

packages/core/src/volar/language-server.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,22 @@ connection.onInitialize((params) => {
7474
if (uri.scheme === 'file') {
7575
// Use tsserver to find the tsconfig governing this file.
7676
const fileName = uri.fsPath.replace(/\\/g, '/');
77-
const projectInfo = await sendTsServerRequest<ts.server.protocol.ProjectInfo>(
77+
let projectInfo = await sendTsServerRequest<ts.server.protocol.ProjectInfo>(
7878
'_glint:' + ts.server.protocol.CommandTypes.ProjectInfo,
7979
{
8080
file: fileName,
8181
needFileNameList: false,
8282
} satisfies ts.server.protocol.ProjectInfoRequestArgs,
8383
);
84+
if (!projectInfo) {
85+
projectInfo = await sendTsServerRequest<ts.server.protocol.ProjectInfo>(
86+
ts.server.protocol.CommandTypes.ProjectInfo,
87+
{
88+
file: fileName,
89+
needFileNameList: false,
90+
} satisfies ts.server.protocol.ProjectInfoRequestArgs,
91+
);
92+
}
8493
if (projectInfo) {
8594
const { configFileName } = projectInfo;
8695
let ls = tsconfigProjects.get(URI.file(configFileName));

0 commit comments

Comments
 (0)