forked from IgnaceMaes/ember-codemod-template-tag
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Hardcoded a bunch of thins to make it work with Discourse – the chat plugin to be specific. We have a bunch of custom resolver rules that needs to be ported over to make this work. The overall strategry should be generalizable. We have a bunch of custom resolver logic that needs to be ported over, the average app can probably try to share code with the Embroider resolver – or use the Resolver from `@embroider/core` directly with `.resolver.json`. See discourse/discourse#24260 for how this code was used in context.
- Loading branch information
1 parent
c9e68e4
commit 0c1ef12
Showing
10 changed files
with
930 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,93 @@ | ||
import { changeExtension } from './steps/change-extension.js'; | ||
import { convertTests, createOptions } from './steps/index.js'; | ||
import { removeHbsImport } from './steps/remove-import.js'; | ||
import { execSync } from 'node:child_process'; | ||
|
||
import { findFiles } from '@codemod-utils/files'; | ||
|
||
import finalize from './steps/finalize.js'; | ||
import { createOptions } from './steps/index.js'; | ||
import inlineTemplate from './steps/inline-template.js'; | ||
import renameJsToGjs from './steps/rename-js-to-gjs.js'; | ||
import resolveImports from './steps/resolve-imports.js'; | ||
import type { CodemodOptions } from './types/index.js'; | ||
|
||
export function runCodemod(codemodOptions: CodemodOptions): void { | ||
const options = createOptions(codemodOptions); | ||
|
||
convertTests(options); | ||
changeExtension(options); | ||
removeHbsImport(options); | ||
const candidates = findFiles('**/*.hbs', { | ||
ignoreList: ['**/templates/**/*.hbs'], | ||
projectRoot: codemodOptions.projectRoot, | ||
}); | ||
|
||
const converted: string[] = []; | ||
const skipped: [string, string][] = []; | ||
|
||
for (const candidate of candidates) { | ||
const goodRef = execSync('git rev-parse HEAD', { | ||
cwd: options.projectRoot, | ||
encoding: 'utf8', | ||
}).trim(); | ||
|
||
try { | ||
console.log(`Converting ${candidate}`); | ||
|
||
execSync(`git reset --hard ${goodRef}`, { | ||
cwd: options.projectRoot, | ||
encoding: 'utf8', | ||
stdio: 'ignore', | ||
}); | ||
|
||
options.filename = candidate; | ||
|
||
renameJsToGjs(options); | ||
resolveImports(options); | ||
inlineTemplate(options); | ||
finalize(options); | ||
|
||
console.log( | ||
execSync(`git show --color HEAD~`, { | ||
cwd: options.projectRoot, | ||
encoding: 'utf8', | ||
}), | ||
); | ||
|
||
converted.push(candidate); | ||
} catch (error: unknown) { | ||
let reason = String(error); | ||
|
||
if (error instanceof Error) { | ||
reason = error.message; | ||
} | ||
|
||
reason = reason.trim(); | ||
|
||
console.warn(`Failed to convert ${candidate}: ${reason}`); | ||
|
||
execSync(`git reset --hard ${goodRef}`, { | ||
cwd: options.projectRoot, | ||
stdio: 'ignore', | ||
}); | ||
|
||
skipped.push([candidate, reason]); | ||
} | ||
} | ||
|
||
if (converted.length) { | ||
console.log('Successfully converted %d files:\n', converted.length); | ||
|
||
for (const file of converted) { | ||
console.log(`- ${file}`); | ||
} | ||
|
||
console.log('\n'); | ||
} | ||
|
||
if (skipped.length) { | ||
console.log('Skipped %d files:\n', skipped.length); | ||
|
||
for (const [file, reason] of skipped) { | ||
console.log(`- ${file}`); | ||
console.log(` ${reason}`); | ||
} | ||
|
||
console.log('\n'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,11 @@ | ||
import type { CodemodOptions, Options } from '../types/index.js'; | ||
|
||
export function createOptions(codemodOptions: CodemodOptions): Options { | ||
const { appName: appName, projectRoot } = codemodOptions; | ||
const { appName, filename, projectRoot } = codemodOptions; | ||
|
||
return { | ||
appName: appName, | ||
appName, | ||
filename, | ||
projectRoot, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { execSync } from 'node:child_process'; | ||
import { unlinkSync } from 'node:fs'; | ||
import path from 'node:path'; | ||
|
||
import type { Options } from '../types/index.js'; | ||
|
||
export default function finalize(options: Options): void { | ||
const hbs = path.join(options.projectRoot, options.filename); | ||
|
||
const gjs = path.join( | ||
options.projectRoot, | ||
options.filename.replace('.hbs', '.gjs'), | ||
); | ||
|
||
execSync(`yarn eslint --fix ${JSON.stringify(gjs)}`, { | ||
cwd: options.projectRoot, | ||
}); | ||
|
||
execSync(`yarn prettier --write ${JSON.stringify(gjs)}`, { | ||
cwd: options.projectRoot, | ||
}); | ||
|
||
execSync(`yarn ember-template-lint --fix ${JSON.stringify(hbs)}`, { | ||
cwd: options.projectRoot, | ||
}); | ||
|
||
execSync(`yarn prettier --write ${JSON.stringify(hbs)}`, { | ||
cwd: options.projectRoot, | ||
}); | ||
|
||
const label = path.basename(options.filename).replace('.hbs', ''); | ||
const convert = JSON.stringify(`DEV: convert chat/${label} -> gjs`); | ||
|
||
execSync('git add .', { cwd: options.projectRoot }); | ||
execSync(`git commit -m ${convert}`, { | ||
cwd: options.projectRoot, | ||
stdio: 'ignore', | ||
}); | ||
|
||
unlinkSync(hbs); | ||
|
||
const cleanup = JSON.stringify(`DEV: cleanup chat/${label}`); | ||
|
||
execSync('git add .', { cwd: options.projectRoot }); | ||
execSync(`git commit -m ${cleanup}`, { | ||
cwd: options.projectRoot, | ||
stdio: 'ignore', | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { readFileSync, writeFileSync } from 'node:fs'; | ||
import path from 'node:path'; | ||
|
||
import { AST } from '@codemod-utils/ast-javascript'; | ||
|
||
import type { Options } from '../types/index.js'; | ||
|
||
export default function inlineTemplate(options: Options): void { | ||
const hbs = path.join(options.projectRoot, options.filename); | ||
|
||
const hbsSource = readFileSync(hbs, { encoding: 'utf8' }); | ||
|
||
const gjs = path.join( | ||
options.projectRoot, | ||
options.filename.replace('.hbs', '.gjs'), | ||
); | ||
|
||
const jsSource = readFileSync(gjs, { encoding: 'utf8' }); | ||
|
||
const traverse = AST.traverse(false); | ||
|
||
let found = 0; | ||
|
||
const ast = traverse(jsSource, { | ||
visitClassDeclaration(path) { | ||
if ( | ||
path.node.superClass?.type === 'Identifier' && | ||
path.node.superClass.name === 'Component' | ||
) { | ||
found++; | ||
|
||
path.node.body.body.push( | ||
AST.builders.classProperty( | ||
AST.builders.identifier('__template__'), | ||
null, | ||
), | ||
); | ||
} | ||
|
||
return false; | ||
}, | ||
}); | ||
|
||
if (found === 0) { | ||
throw new Error('cannot find class'); | ||
} else if (found > 1) { | ||
throw new Error('too many classes?'); | ||
} | ||
|
||
const jsPlaceholder = AST.print(ast); | ||
|
||
const matches = [...jsPlaceholder.matchAll(/^\s*__template__;$/gm)]; | ||
|
||
if (matches.length === 0) { | ||
throw new Error('cannot find placeholder'); | ||
} else if (matches.length > 1) { | ||
throw new Error('too many placeholders?'); | ||
} | ||
|
||
let templateTag = `\n`; | ||
|
||
templateTag += ` <template>\n`; | ||
|
||
for (const line of hbsSource.split('\n')) { | ||
templateTag += ` ${line}\n`; | ||
} | ||
|
||
templateTag += ` </template>`; | ||
|
||
const gjsSource = jsPlaceholder.replace(/^\s*__template__;$/m, templateTag); | ||
|
||
writeFileSync(gjs, gjsSource, { encoding: 'utf8' }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { execSync } from 'node:child_process'; | ||
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'; | ||
import path from 'node:path'; | ||
|
||
import type { Options } from '../types/index.js'; | ||
|
||
export default function renameJsToGjs(options: Options): void { | ||
const src = path.join( | ||
options.projectRoot, | ||
options.filename.replace('.hbs', '.js'), | ||
); | ||
|
||
const dest = path.join( | ||
options.projectRoot, | ||
options.filename.replace('.hbs', '.gjs'), | ||
); | ||
|
||
if (existsSync(src)) { | ||
if (readFileSync(src, { encoding: 'utf8' }).includes(`.extend(`)) { | ||
throw new Error( | ||
`It appears to be a classic class, convert it to native class first!`, | ||
); | ||
} | ||
|
||
renameSync(src, dest); | ||
} else { | ||
if (options.filename.includes('/components/')) { | ||
writeFileSync( | ||
dest, | ||
`import Component from "@glimmer/component";\n` + | ||
`\n` + | ||
`export default class extends Component {\n` + | ||
`}\n`, | ||
{ encoding: 'utf8' }, | ||
); | ||
} else { | ||
throw new Error(`It does not appear to be a component!`); | ||
} | ||
} | ||
|
||
const label = path.basename(options.filename).replace('.hbs', ''); | ||
const message = JSON.stringify(`DEV: mv chat/${label} -> gjs`); | ||
|
||
execSync('git add .', { cwd: options.projectRoot }); | ||
execSync(`git commit -m ${message}`, { | ||
cwd: options.projectRoot, | ||
stdio: 'ignore', | ||
}); | ||
} |
Oops, something went wrong.