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'); + } }