Skip to content
Merged
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
42 changes: 1 addition & 41 deletions packages/core/src/transform/template/rewrite-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,6 @@ import { calculateCompanionTemplateSpans } from './inlining/companion-file.js';
*/
export type RewriteInput = { script: SourceFile; template?: SourceFile };

// HACK: We prefix every transformed TS file with these non-existent imports
// because it causes TypeScript to consider `.gts` and `.gjs` as possible
// implied extensions when extensions are omitted from import module specifiers,
// i.e. it causes `import FooComponent from './foo';` to work given a `foo.gts` file.
//
// Origin of this hack:
// https://github.com/typed-ember/glint/issues/806#issuecomment-2758616327
//
// This approach has the following desirable properties:
//
// 1. It doesn't break Organize Imports command
// 2. It doesn't introduce any keywords/variables that'll show up in auto-complete suggestions
const EXTENSION_FIXING_HEADER_HACK_GTS = `
// @ts-expect-error
({} as typeof import('./__glint-hacky-nonexistent.gts'));

// @ts-expect-error
({} as typeof import('./__glint-hacky-nonexistent.gjs'));

`;

const EXTENSION_FIXING_HEADER_HACK_GJS = `
// @ts-expect-error
(/** @type {typeof import("./__glint-hacky-nonexistent.gts")} */ ({}))

// @ts-expect-error
(/** @type {typeof import("./__glint-hacky-nonexistent.gjs")} */ ({}))

`;

/**
* Given the script and/or template that together comprise a component module,
* returns a `TransformedModule` representing the combined result, with the
Expand Down Expand Up @@ -97,17 +67,7 @@ function calculateCorrelatedSpans(
): CorrelatedSpansResult {
let directives: Array<Directive> = [];
let errors: Array<TransformError> = [];
let partialSpans: Array<PartialCorrelatedSpan> = [
{
originalFile: script,
originalStart: 0,
originalLength: 0,
insertionPoint: 0,
transformedSource: environment.isUntypedScript(script.filename)
? EXTENSION_FIXING_HEADER_HACK_GJS
: EXTENSION_FIXING_HEADER_HACK_GTS,
},
];
let partialSpans: Array<PartialCorrelatedSpan> = [];

let { ast, emitMetadata, error } = parseScript(ts, script, environment);

Expand Down
43 changes: 43 additions & 0 deletions packages/tsserver-plugin/src/typescript-server-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,49 @@ const plugin = createLanguageServicePlugin(
(fileName) => fileName,
);

const resolveModuleNameLiterals =
info.languageServiceHost.resolveModuleNameLiterals?.bind(info.languageServiceHost);

if (resolveModuleNameLiterals) {
// TS isn't aware of our customer .gts/.gjs extensions by default which causes
// issues with resolving imports that omit extensions. We hackishly "teach"
// TS about these extensions by overriding `resolveModuleNameLiterals` to
// inject non-existent imports that cause TS to consider the extensions when
// resolving.
//
// Origin of this hack:
// https://github.com/typed-ember/glint/issues/806#issuecomment-2758616327
info.languageServiceHost.resolveModuleNameLiterals = (
moduleLiterals,
containingFile,
redirectedReference,
options,
...rest
) => {
let fakeImportNodes: any = [];
if (moduleLiterals.length > 0) {
fakeImportNodes.push({
...moduleLiterals[0],
text: './__NONEXISTENT_GLINT_HACK__.gts',
});
fakeImportNodes.push({
...moduleLiterals[0],
text: './__NONEXISTENT_GLINT_HACK__.gjs',
});
}

const result = resolveModuleNameLiterals(
[...fakeImportNodes, ...moduleLiterals],
containingFile,
redirectedReference,
options,
...rest,
);

return result.slice(fakeImportNodes.length);
};
}

// #3963
// const timer = setInterval(() => {
// if (info.project['program']) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Greeting from './Greeting';

export default Greeting;
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import '@glint/environment-ember-loose';
import '@glint/environment-ember-template-imports';

// TS-PLUGIN: I had to add the .gts extension in order for this to work, otherwise
// Cannot find module './Greeting' or its corresponding type declarations. [ts-plugin(2307)]

// import Greeting from './Greeting';
import Greeting from './Greeting.gts';
import Greeting from './Greeting';

<template>
<Greeting @target="World" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { stripIndent } from 'common-tags';
describe('Language Server: Imports', () => {
afterEach(teardownSharedTestWorkspaceAfterEach);

test('support moduleResolution=bundler index.gts resolution from gts file with <template>', async () => {
test('support moduleResolution=bundler importing from gts file with <template>', async () => {
const code = stripIndent`
import Colocated from './colocated';

Expand All @@ -26,7 +26,7 @@ describe('Language Server: Imports', () => {
expect(diagnostics).toMatchInlineSnapshot(`[]`);
});

test('support moduleResolution=bundler index.gts resolution from gts file with no <template>', async () => {
test('support moduleResolution=bundler importing from gts file with no <template>', async () => {
const code = stripIndent`
import Colocated from './colocated';

Expand All @@ -41,4 +41,20 @@ describe('Language Server: Imports', () => {

expect(diagnostics).toMatchInlineSnapshot(`[]`);
});

test('support moduleResolution=bundler importing from vanilla TS file', async () => {
const code = stripIndent`
import Colocated from './colocated';

export default Colocated;
`;

const diagnostics = await requestDiagnostics(
'ts-template-imports-app/src/ephemeral-vanilla-typescript.ts',
'glimmer-ts',
code,
);

expect(diagnostics).toMatchInlineSnapshot(`[]`);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--
<head>
<!--
This is a tool that makes it easier to see the line number, column number, and offset (from start of file)
in a blob of text. This is useful for debugging tests and double checking logic that involves precise
character positions (common in Language Tooling).
Expand All @@ -14,122 +14,129 @@
will update, showing the current cursor position and the selected text's position as well
as highlighting the text in the output grid.
-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<textarea id="input" style="width: 140ch; height: 400px;"></textarea>
<div>
<button id="de-indent">de-indent</button>
</div>
<div id="info"></div>
<div id="output"></div>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<textarea id="input" style="width: 140ch; height: 400px"></textarea>
<div>
<button id="de-indent">de-indent</button>
</div>
<div id="info"></div>
<div id="output"></div>

<script>
const input = document.getElementById('input');
const output = document.getElementById('output');
const info = document.getElementById('info');
<script>
const input = document.getElementById('input');
const output = document.getElementById('output');
const info = document.getElementById('info');

function getPositionInfo(text, offset) {
const lines = text.split('\n');
let currentOffset = 0;
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
const line = lines[lineNum];
if (offset >= currentOffset && offset <= currentOffset + line.length) {
const charNum = offset - currentOffset;
return { lineNum, charNum, offset };
function getPositionInfo(text, offset) {
const lines = text.split('\n');
let currentOffset = 0;
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
const line = lines[lineNum];
if (offset >= currentOffset && offset <= currentOffset + line.length) {
const charNum = offset - currentOffset;
return { lineNum, charNum, offset };
}
currentOffset += line.length + 1; // +1 for newline
}
currentOffset += line.length + 1; // +1 for newline
return null;
}
return null;
}

function updateInfo() {
const text = input.value;
const selectionStart = input.selectionStart;
const selectionEnd = input.selectionEnd;
const startPos = getPositionInfo(text, selectionStart);

let infoText = `Cursor - Line: ${startPos.lineNum}, Character: ${startPos.charNum}, Offset: ${startPos.offset}`;

if (selectionStart !== selectionEnd) {
const endPos = getPositionInfo(text, selectionEnd);
infoText += `<br>Selection - From (${startPos.lineNum}:${startPos.charNum}, offset ${startPos.offset}) to (${endPos.lineNum}:${endPos.charNum}, offset ${endPos.offset})`;
infoText += `<br>Selected length: ${selectionEnd - selectionStart}`;
function updateInfo() {
const text = input.value;
const selectionStart = input.selectionStart;
const selectionEnd = input.selectionEnd;
const startPos = getPositionInfo(text, selectionStart);

let infoText = `Cursor - Line: ${startPos.lineNum}, Character: ${startPos.charNum}, Offset: ${startPos.offset}`;

if (selectionStart !== selectionEnd) {
const endPos = getPositionInfo(text, selectionEnd);
infoText += `<br>Selection - From (${startPos.lineNum}:${startPos.charNum}, offset ${startPos.offset}) to (${endPos.lineNum}:${endPos.charNum}, offset ${endPos.offset})`;
infoText += `<br>Selected length: ${selectionEnd - selectionStart}`;
}

info.innerHTML = infoText;
updateDisplay();
}

info.innerHTML = infoText;
updateDisplay();
}

function updateDisplay() {
let offset = 0;
const lines = input.value.split('\n');
lines[lines.length - 1] = lines[lines.length - 1] + '\u0000';

output.innerHTML = lines.map((line, lineNum) => {
const result = (lineNum === lines.length - 1 ? line : line + '\n').split('').map((char, charNum) => {
const currentOffset = offset + charNum;
const displayChar = char === ' ' ? '␣' :
char === '\n' ? '↵' :
char === '\u0000' ? '␀' :
char;
const isAtCursor = currentOffset === input.selectionStart;
const isSelected = currentOffset >= input.selectionStart && currentOffset < input.selectionEnd;
const result = `<div style="display: inline-block; font-family: monospace; border: 1px solid #ccc; margin: 1px; padding: 2px; ${isAtCursor ? 'background-color: #b0b0ff;' : ''} ${isSelected ? 'background-color: #b0b0ff;' : ''}">
function updateDisplay() {
let offset = 0;
const lines = input.value.split('\n');
lines[lines.length - 1] = lines[lines.length - 1] + '\u0000';

output.innerHTML = lines
.map((line, lineNum) => {
const result =
(lineNum === lines.length - 1 ? line : line + '\n')
.split('')
.map((char, charNum) => {
const currentOffset = offset + charNum;
const displayChar =
char === ' ' ? '␣' : char === '\n' ? '↵' : char === '\u0000' ? '␀' : char;
const isAtCursor = currentOffset === input.selectionStart;
const isSelected =
currentOffset >= input.selectionStart && currentOffset < input.selectionEnd;
const result = `<div style="display: inline-block; font-family: monospace; border: 1px solid #ccc; margin: 1px; padding: 2px; ${
isAtCursor ? 'background-color: #b0b0ff;' : ''
} ${isSelected ? 'background-color: #b0b0ff;' : ''}">
<div style="font-size: 16px;">${displayChar}</div>
<div style="font-size: 10px; color: #666;">
${lineNum}<br>
${charNum}<br>
${currentOffset}
</div>
</div>`;
return result;
}).join('') + '<br>';
offset += line.length + 1; // +1 for newline
return result;
}).join('');
}
return result;
})
.join('') + '<br>';
offset += line.length + 1; // +1 for newline
return result;
})
.join('');
}

input.addEventListener('input', updateInfo);
input.addEventListener('click', updateInfo);
input.addEventListener('keyup', updateInfo);
input.addEventListener('select', updateInfo);
input.addEventListener('input', updateInfo);
input.addEventListener('click', updateInfo);
input.addEventListener('keyup', updateInfo);
input.addEventListener('select', updateInfo);

document.getElementById('de-indent').addEventListener('click', () => {
const lines = input.value.split('\n');
if (lines.length === 0) return;
document.getElementById('de-indent').addEventListener('click', () => {
const lines = input.value.split('\n');
if (lines.length === 0) return;

// Find number of leading spaces in first line
const firstLine = lines[0];
let leadingSpaces = 0;
for (let i = 0; i < firstLine.length; i++) {
if (firstLine[i] === ' ') {
leadingSpaces++;
} else {
break;
// Find number of leading spaces in first line
const firstLine = lines[0];
let leadingSpaces = 0;
for (let i = 0; i < firstLine.length; i++) {
if (firstLine[i] === ' ') {
leadingSpaces++;
} else {
break;
}
}
}

if (leadingSpaces === 0) return;
if (leadingSpaces === 0) return;

// De-indent all lines by that amount, but don't remove non-spaces
const deindented = lines.map(line => {
let spacesToRemove = leadingSpaces;
let i = 0;
// Only remove up to leadingSpaces number of spaces from start
while (spacesToRemove > 0 && i < line.length && line[i] === ' ') {
i++;
spacesToRemove--;
}
return line.slice(i);
});
// De-indent all lines by that amount, but don't remove non-spaces
const deindented = lines.map((line) => {
let spacesToRemove = leadingSpaces;
let i = 0;
// Only remove up to leadingSpaces number of spaces from start
while (spacesToRemove > 0 && i < line.length && line[i] === ' ') {
i++;
spacesToRemove--;
}
return line.slice(i);
});

input.value = deindented.join('\n');
// Trigger input event to update display
input.dispatchEvent(new Event('input'));
});
</script>
</body>
input.value = deindented.join('\n');
// Trigger input event to update display
input.dispatchEvent(new Event('input'));
});
</script>
</body>
</html>
Loading
Loading