Skip to content

Commit

Permalink
Infer TypeScript JSX syntax based on file extension
Browse files Browse the repository at this point in the history
  • Loading branch information
timkendrick committed Dec 13, 2023
1 parent 043dbf1 commit cd44701
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 207 deletions.
2 changes: 1 addition & 1 deletion packages/ast/src/types/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface AstTransformContext<S extends object = object> extends FileMeta
}

export interface FileMetadata {
filename: string | undefined;
filename: string;
}

export interface AstCliContext extends FsContext {
Expand Down
6 changes: 3 additions & 3 deletions packages/codemod-utils/src/angularHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function parseAngularComponentTemplate(
const templateSource = getAngularComponentMetadataTemplateSource(componentMetadata, context);
if (!templateSource) return null;
return parse(templateSource, {
filePath: filePath,
filePath,
suppressParseErrors: false,
});
}
Expand All @@ -72,7 +72,7 @@ function getAngularComponentMetadataTemplateSource(
// FIXME: confirm assumptions on what constitutes a valid Angular templateUrl
// FIXME: warn when unable to load Angular component template
if (!templateUrl || !templateUrl.startsWith('.')) return null;
const currentPath = context.filename ? path.dirname(context.filename) : '.';
const currentPath = path.dirname(context.filename);
const templatePath = path.join(currentPath, templateUrl);
const templateSource = (() => {
const { fs } = context.opts;
Expand All @@ -82,7 +82,7 @@ function getAngularComponentMetadataTemplateSource(
throw new Error(
[
`Failed to load Angular component template: ${templatePath}`,
...(context.filename ? [` in component ${context.filename}`] : []),
` in component ${context.filename}`,
].join('\n'),
{
cause: error,
Expand Down
37 changes: 24 additions & 13 deletions packages/codemod-utils/src/transform/js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,63 +13,74 @@ import {
} from '@ag-grid-devtools/ast';
import { parse, print } from 'recast';

import { type AstTransformOptions } from '../types';
import {
type AstTransformJsOptions,
type AstTransformJsxOptions,
type AstTransformOptions,
} from '../types';

const PARSER_PLUGINS: Array<ParserPlugin> = ['jsx', 'typescript', 'decorators-legacy'];
const JS_PARSER_PLUGINS: Array<ParserPlugin> = ['typescript', 'decorators-legacy'];
const JSX_PARSER_PLUGINS: Array<ParserPlugin> = ['jsx', ...JS_PARSER_PLUGINS];

export function transformJsScriptFile(
source: string,
transforms: Array<AstTransform<AstCliContext> | AstTransformWithOptions<AstCliContext>>,
options: AstTransformOptions,
options: AstTransformOptions & AstTransformJsOptions & AstTransformJsxOptions,
): AstTransformResult {
return transformJsFile(source, transforms, {
...options,
sourceType: options.sourceType || 'script',
sourceType: 'script',
});
}

export function transformJsModuleFile(
source: string,
transforms: Array<AstTransform<AstCliContext> | AstTransformWithOptions<AstCliContext>>,
options: AstTransformOptions,
options: AstTransformOptions & AstTransformJsOptions & AstTransformJsxOptions,
): AstTransformResult {
return transformJsFile(source, transforms, {
...options,
sourceType: options.sourceType || 'module',
sourceType: 'module',
});
}

export function transformJsUnknownFile(
source: string,
transforms: Array<AstTransform<AstCliContext> | AstTransformWithOptions<AstCliContext>>,
options: AstTransformOptions,
options: AstTransformOptions & AstTransformJsOptions & AstTransformJsxOptions,
): AstTransformResult {
return transformJsFile(source, transforms, {
...options,
// Determine whether module/script based on file contents
sourceType: options.sourceType || 'unambiguous',
sourceType: 'unambiguous',
});
}

export function transformJsFile(
source: string,
transforms: Array<AstTransform<AstCliContext> | AstTransformWithOptions<AstCliContext>>,
options: AstTransformOptions & Required<Pick<ParserOptions, 'sourceType'>>,
options: AstTransformOptions &
AstTransformJsOptions &
AstTransformJsxOptions &
Required<Pick<ParserOptions, 'sourceType'>>,
): AstTransformResult {
const { applyDangerousEdits, fs, ...parserOptions } = options;
const { filename, applyDangerousEdits, fs, jsx, sourceType, js: parserOptions = {} } = options;
const defaultPlugins = jsx ? JSX_PARSER_PLUGINS : JS_PARSER_PLUGINS;
// Attempt to determine input file line endings, defaulting to the operating system default
const crlfLineEndings = source.includes('\r\n');
const lfLineEndings = !crlfLineEndings && source.includes('\n');
const lineTerminator = crlfLineEndings ? '\r\n' : lfLineEndings ? '\n' : undefined;
// Parse the source AST
const ast = parse(source, {
parser: {
sourceFilename: parserOptions.sourceFilename,
sourceFilename: filename,
parse(source: string): ReturnType<typeof parseAst> {
const { plugins } = parserOptions;
return parseAst(source, {
...parserOptions,
plugins: plugins ? [...PARSER_PLUGINS, ...plugins] : PARSER_PLUGINS,
sourceType,
sourceFilename: filename,
plugins: plugins ? [...defaultPlugins, ...plugins] : defaultPlugins,
tokens: true,
});
},
Expand All @@ -78,7 +89,7 @@ export function transformJsFile(
// Transform the AST
const uniqueErrors = new Map<string, SyntaxError>();
const transformContext: AstTransformContext<AstCliContext> = {
filename: parserOptions.sourceFilename,
filename,
opts: {
applyDangerousEdits,
warn(node: NodePath<AstNode> | null, message: string) {
Expand Down
12 changes: 8 additions & 4 deletions packages/codemod-utils/src/transform/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {
type AstTransformWithOptions,
} from '@ag-grid-devtools/ast';

import { type AstTransformOptions } from '../types';
import { type AstTransformJsOptions, type AstTransformOptions } from '../types';
import {
parseVueSfcComponent,
printVueTemplate,
type AST,
type VueTemplateNode,
printVueTemplate,
} from '../vueHelpers';
import { transformJsFile } from './js';

Expand All @@ -28,7 +28,7 @@ export function transformVueSfcFile(
| AstTransform<AstCliContext & VueComponentCliContext>
| AstTransformWithOptions<AstCliContext & VueComponentCliContext>
>,
options: AstTransformOptions,
options: AstTransformOptions & AstTransformJsOptions,
): AstTransformResult {
// Extract the different sections of the SFC
const component = parseVueSfcComponent(source);
Expand Down Expand Up @@ -61,7 +61,11 @@ export function transformVueSfcFile(
const [plugin, options] = Array.isArray(transform) ? transform : [transform, {}];
return [plugin, { ...options, ...vueTransformOptions }];
}),
{ ...options, sourceType: options.sourceType || 'module' },
{
...options,
sourceType: 'module',
jsx: false,
},
);
// Determine whether the template has been updated within the transform
const updatedTemplate = vueTransformOptions.vue && vueTransformOptions.vue.template;
Expand Down
32 changes: 25 additions & 7 deletions packages/codemod-utils/src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,42 @@ import {
transformJsUnknownFile,
transformVueSfcFile,
} from './transform';
import { type AstTransformOptions } from './types';
import { type AstTransformJsOptions, type AstTransformOptions } from './types';

export function transformFile(
source: string,
transforms: Array<AstTransform<AstCliContext> | AstTransformWithOptions<AstCliContext>>,
options: AstTransformOptions,
options: AstTransformOptions & AstTransformJsOptions,
): AstTransformResult {
const extension = extname(options.sourceFilename);
const extension = extname(options.filename);
switch (extension) {
case '.cjs':
return transformJsScriptFile(source, transforms, options);
case '.js':
case '.jsx':
return transformJsUnknownFile(source, transforms, options);
return transformJsUnknownFile(source, transforms, {
// JSX syntax is a superset of JS syntax, so assume all JS input files potentially contain JSX
jsx: true,
...options,
});
case '.cjs':
return transformJsScriptFile(source, transforms, {
// JSX syntax is a superset of JS syntax, so assume all JS input files potentially contain JSX
jsx: true,
...options,
});
case '.mjs':
return transformJsModuleFile(source, transforms, {
// JSX syntax is a superset of JS syntax, so assume all JS input files potentially contain JSX
jsx: true,
...options,
});
case '.ts':
case '.tsx':
return transformJsModuleFile(source, transforms, options);
return transformJsModuleFile(source, transforms, {
// See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#new-tsx-file-extension-and-as-operator
// Legacy TypeScript cast syntax conflicts with JSX syntax, so only enable JSX parsing for .tsx files
jsx: extension === '.tsx',
...options,
});
case '.vue':
return transformVueSfcFile(source, transforms, options);
default:
Expand Down
14 changes: 10 additions & 4 deletions packages/codemod-utils/src/types/transform.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { type ParserOptions } from '@ag-grid-devtools/ast';
import { type FileMetadata, type ParserOptions } from '@ag-grid-devtools/ast';
import { type CodemodFsUtils } from '@ag-grid-devtools/types';

export type AstTransformOptions = ParserOptions &
Required<Pick<ParserOptions, 'sourceFilename'>> &
AstTransformCliOptions;
export type AstTransformOptions = FileMetadata & AstTransformCliOptions;

export interface AstTransformCliOptions {
applyDangerousEdits: boolean;
fs: CodemodFsUtils;
}

export interface AstTransformJsOptions {
js?: Omit<ParserOptions, 'sourceFilename' | 'sourceType'>;
}

export interface AstTransformJsxOptions {
jsx: boolean;
}
21 changes: 21 additions & 0 deletions packages/codemods/src/versions/31.0.0/codemod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,27 @@ describe('Retains line endings', () => {
});
});

test('Supports legacy TypeScript cast expressions in non-TSX files', () => {
const input = `
import { Grid, GridOptions } from 'ag-grid-community';
const options = <GridOptions>{ foo: true };
new Grid(document.body, options);
`;
const expected = `
import { GridOptions, createGrid } from 'ag-grid-community';
const options = <GridOptions>{ foo: true };
const optionsApi = createGrid(document.body, options);
`;
const actual = codemod(
{ path: './input.ts', source: input },
{
applyDangerousEdits: true,
fs: createFsHelpers(memfs),
},
);
expect(actual).toEqual({ source: expected, errors: [] });
});

function createFsHelpers(fs: typeof memfs): CodemodFsUtils {
return {
readFileSync,
Expand Down
2 changes: 1 addition & 1 deletion packages/codemods/src/versions/31.0.0/codemod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const codemod: Codemod = function transform(
const { path, source } = file;
const { applyDangerousEdits, fs } = options;
return transformFile(source, transforms, {
sourceFilename: path,
filename: path,
applyDangerousEdits: Boolean(applyDangerousEdits),
fs,
});
Expand Down
Loading

0 comments on commit cd44701

Please sign in to comment.