diff --git a/package.json b/package.json index 336711e..bb6f636 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "chalk": "^4.0.0", + "content-tag": "^2.0.2", "remove-types": "^1.0.0" }, "devDependencies": { diff --git a/src/typescript-blueprint-polyfill.js b/src/typescript-blueprint-polyfill.js index 0d826b9..ce3b6e7 100644 --- a/src/typescript-blueprint-polyfill.js +++ b/src/typescript-blueprint-polyfill.js @@ -1,6 +1,10 @@ -const { removeTypes } = require('remove-types'); const chalk = require('chalk'); -const { replaceExtension, isTypeScriptFile } = require('./utils'); +const path = require('path'); +const { + replaceExtension, + replaceTypeScriptExtension, + isTypeScriptFile, +} = require('./utils'); module.exports = function (context) { const blueprintClass = context._super.constructor.prototype; @@ -73,12 +77,12 @@ module.exports = function (context) { context.convertToJS = async function (fileInfo) { let rendered = await fileInfo.render(); - const transformed = await removeTypes(rendered); - - fileInfo.rendered = transformed; - - fileInfo.displayPath = replaceExtension(fileInfo.displayPath, '.js'); - fileInfo.outputPath = replaceExtension(fileInfo.outputPath, '.js'); + fileInfo.rendered = await removeTypes( + path.extname(fileInfo.displayPath), + rendered + ); + fileInfo.displayPath = replaceTypeScriptExtension(fileInfo.displayPath); + fileInfo.outputPath = replaceTypeScriptExtension(fileInfo.outputPath); return fileInfo; }; @@ -147,3 +151,81 @@ module.exports = function (context) { }, []); }; }; + +/** + Removes types from .ts and .gts files. + Based on the code in ember-cli: https://github.com/ember-cli/ember-cli/blob/2dc099a90dc0a4e583e43e4020d691b342f1e891/lib/models/blueprint.js#L531 + + @private + @method removeTypes + @param {string} extension + @param {string} code + @return {Promise} + */ +async function removeTypes(extension, code) { + const { removeTypes: removeTypesFn } = require('remove-types'); + + if (extension === '.gts') { + const { Preprocessor } = require('content-tag'); + const preprocessor = new Preprocessor(); + // Strip template tags + const templateTagIdentifier = (index) => + `template = __TEMPLATE_TAG_${index}__;`; + const templateTagIdentifierBraces = (index) => + `(template = __TEMPLATE_TAG_${index}__);`; + const templateTagMatches = preprocessor.parse(code); + let strippedCode = code; + for (let i = 0; i < templateTagMatches.length; i++) { + const match = templateTagMatches[i]; + const templateTag = substringBytes( + code, + match.range.start, + match.range.end + ); + strippedCode = strippedCode.replace( + templateTag, + templateTagIdentifier(i) + ); + } + + // Remove types + const transformed = await removeTypesFn(strippedCode); + + // Readd stripped template tags + let transformedWithTemplateTag = transformed; + for (let i = 0; i < templateTagMatches.length; i++) { + const match = templateTagMatches[i]; + const templateTag = substringBytes( + code, + match.range.start, + match.range.end + ); + transformedWithTemplateTag = transformedWithTemplateTag.replace( + templateTagIdentifier(i), + templateTag + ); + transformedWithTemplateTag = transformedWithTemplateTag.replace( + templateTagIdentifierBraces(i), + templateTag + ); + } + + return transformedWithTemplateTag; + } + + return await removeTypesFn(code); +} + +/** + * Takes a substring of a string based on byte offsets. + * @private + * @method substringBytes + * @param {string} value : The input string. + * @param {number} start : The byte index of the substring start. + * @param {number} end : The byte index of the substring end. + * @return {string} : The substring. + */ +function substringBytes(value, start, end) { + let buf = Buffer.from(value); + return buf.subarray(start, end).toString(); +} diff --git a/src/utils.js b/src/utils.js index c4ccc1b..6bae9b0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -10,11 +10,24 @@ function replaceExtension(filePath, newExt) { }); } +function replaceTypeScriptExtension(filePath) { + const extensionMap = { + '.ts': '.js', + '.gts': '.gjs', + }; + const ext = path.extname(filePath); + const newExt = extensionMap[ext]; + + return replaceExtension(filePath, newExt); +} + function isTypeScriptFile(filePath) { - return path.extname(filePath) === '.ts'; + const extension = path.extname(filePath); + return extension === '.ts' || extension === '.gts'; } module.exports = { replaceExtension, + replaceTypeScriptExtension, isTypeScriptFile, }; diff --git a/test/generate.test.js b/test/generate.test.js index f969843..e63cb9b 100644 --- a/test/generate.test.js +++ b/test/generate.test.js @@ -13,6 +13,28 @@ const JS_FIXTURE = `export default function foo(a, b) { } `; +const GTS_FIXTURE = `import Component from '@glimmer/component'; + +interface Signature { + Args: { + foo: string; + } +} + +export default class Foo extends Component { + bar: string = 'bar'; + +} +`; + +const GJS_FIXTURE = `import Component from '@glimmer/component'; + +export default class Foo extends Component { + bar = 'bar'; + +} +`; + const ROOT = process.cwd(); const EmberCLITargets = ['ember-cli-3-24', 'ember-cli-3-28', 'ember-cli']; @@ -86,6 +108,19 @@ describe('ember generate', () => { return a + b; } `, + '__name__.gts': `import Component from '@glimmer/component'; + +interface Signature { + Args: { + foo: string; + } +} + +export default class <%=classifiedModuleName %> extends Component { + bar: string = 'bar'; + +} +` }, }, }, @@ -98,16 +133,20 @@ describe('ember generate', () => { await ember(['generate', 'my-blueprint', 'foo']); const generated = await file('app/my-blueprints/foo.js'); - expect(generated).toEqual(JS_FIXTURE); + + const generatedGjs = await file('app/my-blueprints/foo.gjs'); + expect(generatedGjs).toEqual(GJS_FIXTURE); }); test('it generates typescript with --typescript', async () => { await ember(['generate', 'my-blueprint', 'foo', '--typescript']); const generated = await file('app/my-blueprints/foo.ts'); - expect(generated).toEqual(TS_FIXTURE); + + const generatedGts = await file('app/my-blueprints/foo.gts'); + expect(generatedGts).toEqual(GTS_FIXTURE); }); test('it generates typescript when isTypeScriptProject is true', async () => { @@ -119,6 +158,9 @@ describe('ember generate', () => { const generated = await file('app/my-blueprints/foo.ts'); expect(generated).toEqual(TS_FIXTURE); + + const generatedGts = await file('app/my-blueprints/foo.gts'); + expect(generatedGts).toEqual(GTS_FIXTURE); }); test('it generates javascript when isTypeScriptProject is explicitly false', async () => { @@ -130,6 +172,9 @@ describe('ember generate', () => { const generated = await file('app/my-blueprints/foo.js'); expect(generated).toEqual(JS_FIXTURE); + + const generatedGjs = await file('app/my-blueprints/foo.gjs'); + expect(generatedGjs).toEqual(GJS_FIXTURE); }); test('it generates typescript if {typescript: true} is present in ember-cli', async () => { @@ -141,6 +186,9 @@ describe('ember generate', () => { const generated = await file('app/my-blueprints/foo.ts'); expect(generated).toEqual(TS_FIXTURE); + + const generatedGts = await file('app/my-blueprints/foo.gts'); + expect(generatedGts).toEqual(GTS_FIXTURE); }); test('does not generate typescript when --no-typescript is passed', async () => { @@ -148,6 +196,9 @@ describe('ember generate', () => { const generated = await file('app/my-blueprints/foo.js'); expect(generated).toEqual(JS_FIXTURE); + + const generatedGjs = await file('app/my-blueprints/foo.gjs'); + expect(generatedGjs).toEqual(GJS_FIXTURE); }); test('does not generate typescript when --no-typescript is passed, even in a typescript project', async () => { @@ -159,6 +210,9 @@ describe('ember generate', () => { const generated = await file('app/my-blueprints/foo.js'); expect(generated).toEqual(JS_FIXTURE); + + const generatedGjs = await file('app/my-blueprints/foo.gjs'); + expect(generatedGjs).toEqual(GJS_FIXTURE); }); }); diff --git a/yarn.lock b/yarn.lock index a0c786c..1d060e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2445,6 +2445,11 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" +content-tag@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/content-tag/-/content-tag-2.0.2.tgz#978802d97df21516daa10d78e2a1f148e89eab8b" + integrity sha512-qHRyTp02dgzRK2tsCFxZ1H289bZOuSLNpupr6prvnSFq4SFPmNlBKbbE5PCMb+8+Z1a1z+yCVtXvQIGUCCa3lQ== + content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"