diff --git a/packages/transform/__tests__/debug.test.ts b/packages/transform/__tests__/debug.test.ts
index b4c44cf0e..1fd6a48b1 100644
--- a/packages/transform/__tests__/debug.test.ts
+++ b/packages/transform/__tests__/debug.test.ts
@@ -277,5 +277,136 @@ describe('Debug utilities', () => {
|"
`);
});
+
+ test('Windows line endings', () => {
+ let script = {
+ filename: 'test.ts',
+ contents: stripIndent`
+ import Component, { hbs } from '@glint/environment-glimmerx/component';
+
+ export default class MyComponent extends Component {
+ private bar = 'hi';
+
+ static template = hbs\`
+
+ \`;
+ }
+
+ class HelperComponent extends Component<{ Args: { foo: string } }> {
+ static template = hbs\`
+
+ Hello, {{@foo}}!
+
+ {{! @glint-expect-error: no @bar arg }}
+ {{@bar}}
+
+ \`;
+ }
+ `.replace(/\n/g, '\r\n'),
+ };
+
+ let transformedModule = rewriteModule({ script }, GlintEnvironment.load('glimmerx'));
+
+ expect(transformedModule?.toDebugString()).toMatchInlineSnapshot(`
+ "TransformedModule
+
+ | Mapping: Template
+ | hbs(174:226): hbs\`\\\\r\\\\n \\\\r\\\\n \`
+ | ts(174:533): ({} as typeof import(\\"@glint/environment-glimmerx/-private/dsl\\")).template(function(𝚪: import(\\"@glint/environment-glimmerx/-private/dsl\\").ResolveContext, χ: typeof import(\\"@glint/environment-glimmerx/-private/dsl\\")) {\\\\n hbs;\\\\n {\\\\n const 𝛄 = χ.emitComponent(χ.resolve(HelperComponent)({ foo: 𝚪.this.bar }));\\\\n 𝛄;\\\\n }\\\\n 𝚪; χ;\\\\n}) as unknown
+ |
+ | | Mapping: Identifier
+ | | hbs(174:174):
+ | | ts(328:339): MyComponent
+ | |
+ | | Mapping: ElementNode
+ | | hbs(184:221):
+ | | ts(413:511): {\\\\n const 𝛄 = χ.emitComponent(χ.resolve(HelperComponent)({ foo: 𝚪.this.bar }));\\\\n 𝛄;\\\\n }
+ | |
+ | | | Mapping: Identifier
+ | | | hbs(185:200): HelperComponent
+ | | | ts(458:473): HelperComponent
+ | | |
+ | | | Mapping: AttrNode
+ | | | hbs(201:218): @foo={{this.bar}}
+ | | | ts(477:493): foo: 𝚪.this.bar
+ | | |
+ | | | | Mapping: Identifier
+ | | | | hbs(202:205): foo
+ | | | | ts(477:480): foo
+ | | | |
+ | | | | Mapping: MustacheStatement
+ | | | | hbs(206:218): {{this.bar}}
+ | | | | ts(482:493): 𝚪.this.bar
+ | | | |
+ | | | | | Mapping: PathExpression
+ | | | | | hbs(208:216): this.bar
+ | | | | | ts(482:493): 𝚪.this.bar
+ | | | | |
+ | | | | | | Mapping: Identifier
+ | | | | | | hbs(208:212): this
+ | | | | | | ts(485:489): this
+ | | | | | |
+ | | | | | | Mapping: Identifier
+ | | | | | | hbs(213:216): bar
+ | | | | | | ts(490:493): bar
+ | | | | | |
+ | | | | |
+ | | | |
+ | | |
+ | |
+ |
+
+ | Mapping: Template
+ | hbs(324:455): hbs\`\\\\r\\\\n \\\\r\\\\n Hello, {{@foo}}!\\\\r\\\\n\\\\r\\\\n {{! @glint-expect-error: no @bar arg }}\\\\r\\\\n {{@bar}}\\\\r\\\\n
\\\\r\\\\n \`
+ | ts(631:1095): ({} as typeof import(\\"@glint/environment-glimmerx/-private/dsl\\")).template(function(𝚪: import(\\"@glint/environment-glimmerx/-private/dsl\\").ResolveContext, χ: typeof import(\\"@glint/environment-glimmerx/-private/dsl\\")) {\\\\n hbs;\\\\n {\\\\n const 𝛄 = χ.emitElement(\\"p\\");\\\\n χ.applySplattributes(𝚪.element, 𝛄.element);\\\\n χ.emitValue(χ.resolveOrReturn(𝚪.args.foo)({}));\\\\n χ.emitValue(χ.resolveOrReturn(𝚪.args.bar)({}));\\\\n }\\\\n 𝚪; χ;\\\\n}) as unknown
+ |
+ | | Mapping: Identifier
+ | | hbs(324:324):
+ | | ts(785:800): HelperComponent
+ | |
+ | | Mapping: ElementNode
+ | | hbs(334:450): \\\\r\\\\n Hello, {{@foo}}!\\\\r\\\\n\\\\r\\\\n {{! @glint-expect-error: no @bar arg }}\\\\r\\\\n {{@bar}}\\\\r\\\\n
+ | | ts(874:1073): {\\\\n const 𝛄 = χ.emitElement(\\"p\\");\\\\n χ.applySplattributes(𝚪.element, 𝛄.element);\\\\n χ.emitValue(χ.resolveOrReturn(𝚪.args.foo)({}));\\\\n χ.emitValue(χ.resolveOrReturn(𝚪.args.bar)({}));\\\\n }
+ | |
+ | | | Mapping: AttrNode
+ | | | hbs(337:350): ...attributes
+ | | | ts(913:962): χ.applySplattributes(𝚪.element, 𝛄.element);
+ | | |
+ | | | Mapping: MustacheStatement
+ | | | hbs(366:374): {{@foo}}
+ | | | ts(963:1014): χ.emitValue(χ.resolveOrReturn(𝚪.args.foo)({}))
+ | | |
+ | | | | Mapping: PathExpression
+ | | | | hbs(368:372): @foo
+ | | | | ts(997:1008): 𝚪.args.foo
+ | | | |
+ | | | | | Mapping: Identifier
+ | | | | | hbs(369:372): foo
+ | | | | | ts(1005:1008):foo
+ | | | | |
+ | | | |
+ | | |
+ | | | Mapping: MustacheCommentStatement
+ | | | hbs(385:424): {{! @glint-expect-error: no @bar arg }}
+ | | | ts(1016:1016):
+ | | |
+ | | | Mapping: MustacheStatement
+ | | | hbs(432:440): {{@bar}}
+ | | | ts(1016:1067):χ.emitValue(χ.resolveOrReturn(𝚪.args.bar)({}))
+ | | |
+ | | | | Mapping: PathExpression
+ | | | | hbs(434:438): @bar
+ | | | | ts(1050:1061):𝚪.args.bar
+ | | | |
+ | | | | | Mapping: Identifier
+ | | | | | hbs(435:438): bar
+ | | | | | ts(1058:1061):bar
+ | | | | |
+ | | | |
+ | | |
+ | |
+ |"
+ `);
+ });
});
});
diff --git a/packages/transform/__tests__/offset-mapping.test.ts b/packages/transform/__tests__/offset-mapping.test.ts
index f3a675f13..488858b1b 100644
--- a/packages/transform/__tests__/offset-mapping.test.ts
+++ b/packages/transform/__tests__/offset-mapping.test.ts
@@ -164,6 +164,16 @@ describe('Source-to-source offset mapping', () => {
let module = rewriteCompanionTemplate({ backing, contents: '{{@foo}}' });
expectTokenMapping(module, 'foo');
});
+
+ test('Windows line endings', () => {
+ let module = rewriteCompanionTemplate({
+ backing: 'none',
+ contents: `Hello, !\r\n\r\n{{this.foo}}\r\n`,
+ });
+
+ expectTokenMapping(module, 'World');
+ expectTokenMapping(module, 'foo');
+ });
});
describe('inline template', () => {
@@ -378,6 +388,15 @@ describe('Source-to-source offset mapping', () => {
expectTokenMapping(module, 'name', { occurrence: 1 });
});
});
+
+ test('Windows line endings', () => {
+ let module = rewriteInlineTemplate({
+ contents: `Hello, !\r\n\r\n{{this.foo}}\r\n`,
+ });
+
+ expectTokenMapping(module, 'World');
+ expectTokenMapping(module, 'foo');
+ });
});
describe('spans outside of mapped segments', () => {
diff --git a/packages/transform/src/inlining/tagged-strings.ts b/packages/transform/src/inlining/tagged-strings.ts
index 1582b1200..653571548 100644
--- a/packages/transform/src/inlining/tagged-strings.ts
+++ b/packages/transform/src/inlining/tagged-strings.ts
@@ -29,8 +29,16 @@ export function calculateTaggedTemplateSpans(
assert(path.node.start, 'Missing location info');
assert(path.node.end, 'Missing location info');
+ let contentStart = quasis[0].start;
+ let contentEnd = quasis[0].end;
+ assert(contentStart && contentEnd, 'Missing location info');
+
+ // Access the contents directly from source rather than the AST node, as
+ // template literals' line endings are subject to normalization during parse.
+ let contents = script.contents.slice(contentStart, contentEnd);
+
// Pad the template to account for the tag and surrounding ` characters
- let template = `${''.padStart(tagName.length)} ${quasis[0].value.raw} `;
+ let template = `${''.padStart(tagName.length)} ${contents} `;
// Emit a use of the template tag so it's not considered unused
let preamble = [`${tagName};`];
diff --git a/packages/transform/src/mapping-tree.ts b/packages/transform/src/mapping-tree.ts
index f6fb38aed..4622aedce 100644
--- a/packages/transform/src/mapping-tree.ts
+++ b/packages/transform/src/mapping-tree.ts
@@ -92,17 +92,17 @@ export default class MappingTree {
lines.push(`${indent}Mapping: ${sourceNode.type}`);
lines.push(
- `${indent}${` hbs(${hbsStart}:${hbsEnd}):`.padEnd(15)}${originalSource
- .slice(originalRange.start, originalRange.end)
- .trim()
- .replace(/\n/g, '\\n')}`
+ `${indent}${` hbs(${hbsStart}:${hbsEnd}):`.padEnd(15)}${this.getSourceRange(
+ originalSource,
+ originalRange
+ )}`
);
lines.push(
- `${indent}${` ts(${tsStart}:${tsEnd}):`.padEnd(15)}${transformedSource
- .slice(transformedRange.start, transformedRange.end)
- .trim()
- .replace(/\n/g, '\\n')}`
+ `${indent}${` ts(${tsStart}:${tsEnd}):`.padEnd(15)}${this.getSourceRange(
+ transformedSource,
+ transformedRange
+ )}`
);
lines.push(indent);
@@ -117,4 +117,8 @@ export default class MappingTree {
return lines.map((line) => line.trimEnd()).join('\n');
}
+
+ private getSourceRange(source: string, range: Range): string {
+ return source.slice(range.start, range.end).trim().replace(/\n/g, '\\n').replace(/\r/g, '\\r');
+ }
}