Skip to content

Commit

Permalink
Merge pull request #6 from IgnaceMaes/content-tag-utils
Browse files Browse the repository at this point in the history
  • Loading branch information
IgnaceMaes authored Nov 5, 2023
2 parents 8f3ad36 + 1a6fb50 commit 4b2c033
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-ladybugs-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ember-codemod-template-tag": patch
---

Add utils for template tag AST and move hbs import removal to separate step
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"@codemod-utils/ast-template": "^1.1.0",
"@codemod-utils/files": "^1.1.0",
"change-case": "^5.1.2",
"content-tag": "^1.1.2",
"recast": "^0.23.4",
"yargs": "^17.7.2"
},
"devDependencies": {
Expand Down Expand Up @@ -82,6 +84,9 @@
"pnpm": {
"overrides": {
"eslint-plugin-import@2.29.0>tsconfig-paths": "^4.2.0"
},
"patchedDependencies": {
"@codemod-utils/ast-javascript@1.2.0": "patches/@codemod-utils__ast-javascript@1.2.0.patch"
}
}
}
12 changes: 12 additions & 0 deletions patches/@codemod-utils__ast-javascript@1.2.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
diff --git a/dist/ast/javascript.js b/dist/ast/javascript.js
index fb12fdabf61cdcb199a904972abc3401f3955fc8..499a6f42016953aba891c4f10ada3cc861306ae1 100644
--- a/dist/ast/javascript.js
+++ b/dist/ast/javascript.js
@@ -63,6 +63,7 @@ const tsOptions = {
['pipelineOperator', { proposal: 'minimal' }],
'throwExpressions',
'typescript',
+ 'jsx',
],
};
function getParseOptions(isTypeScript) {
20 changes: 18 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { convertTests, createOptions } from './steps/index.js';
import { removeHbsImport } from './steps/remove-import.js';
import type { CodemodOptions } from './types/index.js';

export function runCodemod(codemodOptions: CodemodOptions): void {
const options = createOptions(codemodOptions);

convertTests(options);
removeHbsImport(options);
}
7 changes: 0 additions & 7 deletions src/steps/convert-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,6 @@ function rewriteHbsTemplateString(
}
return false;
},
visitImportDeclaration(path) {
// Remove the `hbs` helper import
if (path.value.source.value === 'ember-cli-htmlbars') {
path.replace();
}
return false;
},
});
addComponentImports(ast, allComponentNames, config.appName);
addHelperImports(ast, allHelperNames);
Expand Down
49 changes: 49 additions & 0 deletions src/steps/remove-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

import { createFiles, findFiles } from '@codemod-utils/files';

import type { Options } from '../types/index.js';
import { AST as AST_TEMPLATE_TAG } from '../utils/ast/template-tag.js';
import { isTypeScriptFile } from '../utils/general.js';

function removeImport(
file: string,
config: { appName: string; isTypeScript: boolean },
): string {
const traverse = AST_TEMPLATE_TAG.traverse(config.isTypeScript);

const { ast, contentTags } = traverse(file, {
visitImportDeclaration(path) {
if (path.value.source.value === 'ember-cli-htmlbars') {
path.replace();
}
return false;
},
});

return AST_TEMPLATE_TAG.print(ast, contentTags);
}

export function removeHbsImport(options: Options): void {
const { appName, projectRoot } = options;

const filePaths = findFiles('**/*-test.{gjs,gts}', {
projectRoot,
});

const fileMap = new Map(
filePaths.map((filePath) => {
let file = readFileSync(join(projectRoot, filePath), 'utf8');

file = removeImport(file, {
appName,
isTypeScript: isTypeScriptFile(filePath),
});

return [filePath, file];
}),
);

createFiles(fileMap, options);
}
133 changes: 133 additions & 0 deletions src/utils/ast/template-tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { AST as AST_JS } from '@codemod-utils/ast-javascript';
import { Preprocessor } from 'content-tag';
import { types } from 'recast';

type Range = {
end: number;
start: number;
};

type ContentTag = {
contentRange: Range;
contents: string;
endRange: Range;
range: Range; // range = startRange + contentRange + endRange
startRange: Range;
tagName: string;
type: string;
};

type ContentTagPlaceholder = {
contents: string;
id: string;
};

export function parse(file: string) {
const preprocessor = new Preprocessor();

return preprocessor.parse(file) as unknown as ContentTag[];
}

export function replaceContents(
file: string,
options: {
contents: string;
range: Range;
},
): string {
const { contents, range } = options;

return [
file.substring(0, range.start),
'<template>',
contents,
'</template>',
file.substring(range.end),
].join('');
}

function _print(
ast: types.ASTNode,
contentTags: ContentTagPlaceholder[],
): string {
let output = AST_JS.print(ast);

const placeholderContentTags = parse(output);
if (placeholderContentTags.length !== contentTags.length) {
throw new Error('The number of content tags does not match');
}
placeholderContentTags.reverse().forEach((placeholderContentTag) => {
const match = contentTags.find(
(contentTag) => contentTag.id === placeholderContentTag.contents,
);
if (match === undefined) {
throw new Error(
`Expected content tag with id "${placeholderContentTag.contents}" to exist, but no match was found.`,
);
}
output = replaceContents(output, {
contents: match.contents,
range: placeholderContentTag.range,
});
});

return output;
}

interface TraverseTT {
ast: types.ASTNode;
contentTags: ContentTagPlaceholder[];
}

function _traverse(
isTypeScript?: boolean,
): (file: string, visitMethods?: types.Visitor) => TraverseTT {
const originalTraverse = AST_JS.traverse(isTypeScript);
return function (
file: string,
visitMethods?: types.Visitor<unknown> | undefined,
): TraverseTT {
const contentTags = parse(file);
const contentTagPlaceholders: ContentTagPlaceholder[] = [];
contentTags.reverse().forEach((contentTag, index) => {
const placeholderId = `${index}`;
file = replaceContents(file, {
contents: placeholderId,
range: contentTag.range,
});
contentTagPlaceholders.push({
contents: contentTag.contents,
id: placeholderId,
});
});

return {
ast: originalTraverse(file, visitMethods),
contentTags: contentTagPlaceholders,
};
};
}

/**
* Provides methods from `recast` to help you parse and transform
* `*.{gjs,gts}` files.
*
* @example
*
* ```ts
* function transformCode(file: string, isTypeScript: boolean): string {
* const traverse = AST.traverse(isTypeScript);
*
* const { ast, contentTags } = traverse(file, {
* // Use AST.builders to transform the tree
* });
*
* return AST.print(ast, contentTags);
* }
* ```
*/
export const AST = {
builders: AST_JS.builders,
print: _print,
traverse: _traverse,
};
24 changes: 24 additions & 0 deletions tests/steps/remove-import/base-case.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { assertFixture, loadFixture, test } from '@codemod-utils/tests';

import { removeHbsImport } from '../../../src/steps/remove-import.js';
import {
codemodOptions,
options,
} from '../../helpers/shared-test-setups/convert-tests.js';

test('steps | remove-import > base case', function () {
const inputProject = {
'example-test.gjs':
"import { hbs } from 'ember-cli-htmlbars';\n<template>Test</template>\n",
};

const outputProject = {
'example-test.gjs': '<template>Test</template>\n',
};

loadFixture(inputProject, codemodOptions);

removeHbsImport(options);

assertFixture(outputProject, codemodOptions);
});

0 comments on commit 4b2c033

Please sign in to comment.