Skip to content

Commit 2e59521

Browse files
authored
Merge pull request #10 from ember-tooling/better-file-processing
Better file processing
2 parents f2fc396 + c340a01 commit 2e59521

File tree

6 files changed

+215
-78
lines changed

6 files changed

+215
-78
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
},
5050
"dependencies": {
5151
"@embroider/addon-shim": "^1.8.9",
52-
"content-tag-utils": "^0.6.0",
5352
"decorator-transforms": "^2.2.2",
5453
"ember-local-storage-decorator": "github:evoactivity/ember-local-storage-decorator#dist",
5554
"ember-modifier": "^4.2.2",

pnpm-lock.yaml

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/babel/get-file-coordinates.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { Parsed } from 'content-tag';
2+
3+
export type InnerCoordinates = {
4+
line: number;
5+
column: number;
6+
endColumn: number;
7+
endLine: number;
8+
};
9+
10+
/**
11+
* Converts template-relative coordinates to file-absolute coordinates.
12+
*
13+
* Given a template embedded in a file and element coordinates within that template,
14+
* this function calculates the absolute position in the source file.
15+
*
16+
* @param source - The full source file content (string or Buffer)
17+
* @param parsedResult - The parsed template result containing byte ranges
18+
* @param innerCoordinates - Element coordinates within the template (template-relative)
19+
* @returns File-absolute coordinates with line, endLine, column, endColumn
20+
*
21+
* @example
22+
* // Given source file:
23+
* // 1 export class Component extends BaseComponent {
24+
* // 2 <template>
25+
* // 3 <div>Hello</div>
26+
* // 4 </template>
27+
* // 5 }
28+
* //
29+
* // Element at template line 2, column 4 becomes file line 3, column 4
30+
*/
31+
export function getFileCoordinates(
32+
source: string | Buffer,
33+
parsedResult: Parsed,
34+
innerCoordinates: InnerCoordinates,
35+
) {
36+
// Convert source to Buffer
37+
let buffer;
38+
if (typeof source === 'string') {
39+
buffer = Buffer.from(source, 'utf8');
40+
} else if (source instanceof Buffer) {
41+
buffer = source;
42+
} else {
43+
throw new Error(
44+
`Expected first arg to getFileCoordinates to be either a string or buffer`,
45+
);
46+
}
47+
48+
// Extract template location in file
49+
const { contentRange: byteRange } = parsedResult;
50+
const beforeContent = buffer.subarray(0, byteRange.startByte).toString();
51+
const contentBeforeTemplateStart = beforeContent.split('\n');
52+
const lineBeforeTemplateStart = contentBeforeTemplateStart.at(-1) ?? '';
53+
54+
/**
55+
* Template coordinates in the file
56+
* Reminder:
57+
* Rows are 1-indexed
58+
* Columns are 0-indexed
59+
*
60+
* (for when someone inevitably needs to debug this and is comparing
61+
* with their editor (editors typically use 1-indexed columns))
62+
*/
63+
const templateLine = contentBeforeTemplateStart.length;
64+
const templateColumn = lineBeforeTemplateStart?.length;
65+
66+
/**
67+
* Convert template-relative coordinates to file-absolute coordinates
68+
*
69+
* Given the sample source code:
70+
* 1 export class SomeComponent extends Component<Args> {
71+
* 2 <template>
72+
* 3 {{debugger}}
73+
* 4 </template>
74+
* 5 }
75+
*
76+
* The extracted template will be:
77+
* 1
78+
* 2 {{debugger}}
79+
*
80+
* The coordinates of the template in the source file are: { line: 3, column: 14 }.
81+
* The coordinates of the error in the template are: { line: 2, column: 4 }.
82+
*
83+
* Thus, we need to always subtract one before adding the template location.
84+
*/
85+
const line = innerCoordinates.line + templateLine - 1;
86+
const endLine = innerCoordinates.endLine + templateLine - 1;
87+
88+
/**
89+
* Given the sample source code:
90+
* 1 export class SomeComponent extends Component<Args> {
91+
* 2 <template>{{debugger}}
92+
* 3 </template>
93+
* 4 }
94+
*
95+
* The extracted template will be:
96+
* 1 {{debugger}}
97+
*
98+
* The coordinates of the template in the source file are: { line: 3, column: 14 }.
99+
* The coordinates of the error in the template are: { line: 1, column: 0 }.
100+
*
101+
* Thus, if the error is found on the first line of a template,
102+
* then we need to add the column location to the result column location.
103+
*
104+
* Any result > line 1 will not require any column correction.
105+
*/
106+
const column =
107+
innerCoordinates.line === 1
108+
? innerCoordinates.column + templateColumn
109+
: innerCoordinates.column;
110+
const endColumn =
111+
innerCoordinates.line === 1
112+
? innerCoordinates.endColumn + templateColumn
113+
: innerCoordinates.endColumn;
114+
115+
return {
116+
line,
117+
endLine,
118+
column,
119+
endColumn,
120+
};
121+
}

src/babel/rewriteHbs.ts

Lines changed: 63 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,53 @@
11
import recast, { type AST } from 'ember-template-recast';
22
import fs from 'node:fs';
33
import { fixFilename, combineRegexPatterns } from './utils.ts';
4-
import { Transformer } from 'content-tag-utils';
4+
import { Preprocessor, type Parsed } from 'content-tag';
5+
import {
6+
getFileCoordinates,
7+
type InnerCoordinates,
8+
} from './get-file-coordinates.ts';
59

6-
let fileCache = new Map<string, string>();
10+
const fileCache = new Map<string, string>();
11+
const fileModifiedTimes = new Map<string, number>();
712

813
const invalidTagPatterns = [
914
/^:/, // Don't process named blocks (:block-name)
1015
];
11-
1216
const isInvalidTag = combineRegexPatterns(invalidTagPatterns);
1317

18+
const p = new Preprocessor();
19+
const parsedFiles = new Map<string, Parsed[]>();
20+
21+
function isFileCacheValid(filename: string): boolean {
22+
if (!fileCache.has(filename) || !fileModifiedTimes.has(filename)) {
23+
return false;
24+
}
25+
26+
const stats = fs.statSync(filename);
27+
const cachedMtime = fileModifiedTimes.get(filename)!;
28+
return stats.mtimeMs === cachedMtime;
29+
}
30+
31+
function parseFile(filename: string, content: string): Parsed[] {
32+
if (parsedFiles.has(filename) && isFileCacheValid(filename)) {
33+
return parsedFiles.get(filename)!;
34+
}
35+
const parsed = p.parse(content);
36+
parsedFiles.set(filename, parsed);
37+
return parsed;
38+
}
39+
1440
function getFullFileContent(filename: string): string {
15-
if (fileCache.has(filename)) {
41+
if (isFileCacheValid(filename)) {
1642
return fileCache.get(filename)!;
1743
}
1844

1945
const content = fs.readFileSync(filename, 'utf-8');
46+
const stats = fs.statSync(filename);
47+
2048
fileCache.set(filename, content);
49+
fileModifiedTimes.set(filename, stats.mtimeMs);
50+
2151
return content;
2252
}
2353

@@ -26,16 +56,16 @@ function getAllElementNodes(program: AST.Program): AST.ElementNode[] {
2656

2757
recast.traverse(program, {
2858
ElementNode(node: AST.ElementNode) {
59+
if (isInvalidTag(node.tag)) {
60+
return;
61+
}
2962
elementNodes.push(node);
3063
},
3164
});
3265

3366
return elementNodes;
3467
}
3568

36-
let programNodesForFile = new Map<string, AST.Program[]>();
37-
let processedElementsForFile = new Map<string, number>(); // Track processed elements per file
38-
3969
export function templatePlugin(env: { filename: string }) {
4070
const isProduction =
4171
process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'prod';
@@ -57,24 +87,15 @@ export function templatePlugin(env: { filename: string }) {
5787
}
5888

5989
const file = getFullFileContent(env.filename);
60-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
61-
const t = new Transformer(file);
62-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
63-
const expectedProgramCount = t.parseResults.length as number;
64-
90+
const parsedFile = parseFile(env.filename, file);
91+
const expectedProgramCount = parsedFile.length;
6592
const relativePath = fixFilename(env.filename);
6693

94+
let elementNodes: AST.ElementNode[] = [];
95+
6796
return {
6897
Program(node: AST.Program) {
69-
programNodesForFile.set(env.filename, [
70-
...(programNodesForFile.get(env.filename) || []),
71-
node,
72-
]);
73-
74-
// Initialize element counter for this file if needed
75-
if (!processedElementsForFile.has(env.filename)) {
76-
processedElementsForFile.set(env.filename, 0);
77-
}
98+
elementNodes = getAllElementNodes(node);
7899
},
79100
ElementNode(node: AST.ElementNode) {
80101
if (expectedProgramCount === 0) {
@@ -85,34 +106,24 @@ export function templatePlugin(env: { filename: string }) {
85106
return;
86107
}
87108

88-
const innerCoordinates = {
109+
const innerCoordinates: InnerCoordinates = {
89110
line: node.loc.startPosition.line,
90111
column: node.loc.startPosition.column,
91112
endColumn: node.loc.endPosition.column,
92113
endLine: node.loc.endPosition.line,
93114
};
94115

95-
let programNodeIndex = -1;
96-
97-
const programNodes = programNodesForFile.get(env.filename) || [];
98-
99-
programNodes.some((programNode, index) => {
100-
const elementNodes = getAllElementNodes(programNode);
101-
102-
if (elementNodes.includes(node)) {
103-
programNodeIndex = index;
104-
return true;
105-
}
106-
107-
return false;
116+
const parsed = parsedFile.find((program: Parsed) => {
117+
// @ts-expect-error data is accessible but marked private, node.loc.module returns 'an unknown module'
118+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
119+
return program.contents === node.loc.data.source.source;
108120
});
109121

110-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
111-
const coords = t.reverseInnerCoordinatesOf(
112-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
113-
t.parseResults[programNodeIndex],
114-
innerCoordinates,
115-
);
122+
if (!parsed) {
123+
return;
124+
}
125+
126+
const coords = getFileCoordinates(file, parsed, innerCoordinates);
116127

117128
node.attributes.push(
118129
recast.builders.attr(
@@ -121,35 +132,25 @@ export function templatePlugin(env: { filename: string }) {
121132
),
122133
recast.builders.attr(
123134
'data-source-line',
124-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
125135
recast.builders.text(coords.line.toString()),
126136
),
127137
recast.builders.attr(
128138
'data-source-column',
129-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
130139
recast.builders.text((coords.column + 1).toString()),
131140
),
132141
);
133142

134-
// Increment processed element count
135-
const processedCount =
136-
(processedElementsForFile.get(env.filename) || 0) + 1;
137-
processedElementsForFile.set(env.filename, processedCount);
138-
139-
// Count total elements across all programs
140-
const totalElements = programNodes.reduce((sum, program) => {
141-
return sum + getAllElementNodes(program).length;
142-
}, 0);
143-
144-
// Check if we've reached the expected program count AND processed all elements
145-
const currentProgramCount = programNodes.length;
146-
if (
147-
currentProgramCount === expectedProgramCount &&
148-
processedCount === totalElements
149-
) {
150-
programNodesForFile = new Map<string, AST.Program[]>();
151-
processedElementsForFile = new Map<string, number>();
152-
fileCache = new Map<string, string>();
143+
// mark element as processed by removing it from the list
144+
const index = elementNodes.indexOf(node);
145+
if (index > -1) {
146+
elementNodes.splice(index, 1);
147+
}
148+
149+
// If all element nodes have been processed, clear the program contents
150+
// so files with multiple matching templates find the correct index
151+
if (elementNodes.length === 0) {
152+
parsed.contents =
153+
'___________ember-source-lens-cleared-content___________';
153154
}
154155
},
155156
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const Foo = <template>
2+
<div class="foo">
3+
<h1>Hello, World!</h1>
4+
</div>
5+
</template>;
6+
7+
export const Bar = <template>
8+
<div class="foo">
9+
<h1>Hello, World!</h1>
10+
</div>
11+
</template>;

0 commit comments

Comments
 (0)