From 69e4e0be396e3e30d20f05c9f21f8d08e1651484 Mon Sep 17 00:00:00 2001 From: Jess Telford Date: Thu, 27 Oct 2022 15:25:12 +1100 Subject: [PATCH] Shim stylelint's .report() util --- .changeset/seven-zoos-sin.md | 5 + polaris-migrator/README.md | 62 ++++++---- .../replace-spacing-lengths.ts | 117 +++++++++++------- .../tests/replace-spacing-lengths.output.scss | 35 ++++-- .../tests/with-namespace.output.scss | 35 ++++-- polaris-migrator/src/utilities/sass.ts | 84 ++++++++++++- .../{{kebabCase migrationName}}.ts.hbs | 41 +++++- 7 files changed, 283 insertions(+), 96 deletions(-) create mode 100644 .changeset/seven-zoos-sin.md diff --git a/.changeset/seven-zoos-sin.md b/.changeset/seven-zoos-sin.md new file mode 100644 index 00000000000..2d4a22300e0 --- /dev/null +++ b/.changeset/seven-zoos-sin.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris-migrator': minor +--- + +Expose the .report() method to SASS migrations for easier aggregation of discovered issues during a migration run. diff --git a/polaris-migrator/README.md b/polaris-migrator/README.md index ff02ccf2c9b..b6536d2d4a2 100644 --- a/polaris-migrator/README.md +++ b/polaris-migrator/README.md @@ -278,38 +278,50 @@ migrations #### The SASS migration function -Each migrator has a default export adhering to the [PostCSS Plugin API](https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md) with one main difference: events are only executed once. +Each migrator has a default export adhering to the [Stylelint Rule API](https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md). A PostCSS AST is passed as the `root` and can be mutated inline, or emit warning/error reports. Continuing the example, here is what the migration may look like if our goal is to replace the Sass function `hello()` with `world()`. ```ts // polaris-migrator/src/migrations/replace-sass-function/replace-sass-function.ts - -import type {FileInfo} from 'jscodeshift'; -import postcss, {Plugin} from 'postcss'; -import valueParser from 'postcss-value-parser'; - -const plugin = (): Plugin => ({ - postcssPlugin: 'replace-sass-function', - Declaration(decl) { - // const prop = decl.prop; - const parsedValue = valueParser(decl.value); - - parsedValue.walk((node) => { - if (!(node.type === 'function' && node.value === 'hello')) return; - - node.value = 'world'; +import { + isSassFunction, + StopWalkingFunctionNodes, + createSassMigrator, +} from '../../utilities/sass'; +import type {PolarisMigrator} from '../../utilities/sass'; + +const replaceHelloWorld: PolarisMigrator = (_, {methods}, context) => { + return (root) => { + root.walkDecls((decl) => { + const parsedValue = valueParser(decl.value); + parsedValue.walk((node) => { + if (isSassFunction('hello', node)) { + if (context.fix) { + node.value = 'world'; + } else { + methods.report({ + node: decl, + severity: 'error', + message: + 'Method hello() is no longer supported. Please migrate to world().', + }); + } + + return StopWalkingFunctionNodes; + } + }); + + if (context.fix) { + decl.value = parsedValue.toString(); + } + + methods.flushReports(); }); + }; +}; - decl.value = parsedValue.toString(); - }, -}); - -export default function replaceSassFunction(fileInfo: FileInfo) { - return postcss(plugin()).process(fileInfo.source, { - syntax: require('postcss-scss'), - }).css; -} +export default createSassMigrator('replace-hello-world', replaceHelloWorld); ``` A more complete example can be seen in [`replace-spacing-lengths.ts`](https://github.com/Shopify/polaris/blob/main/polaris-migrator/src/migrations/replace-spacing-lengths/replace-spacing-lengths.ts). diff --git a/polaris-migrator/src/migrations/replace-spacing-lengths/replace-spacing-lengths.ts b/polaris-migrator/src/migrations/replace-spacing-lengths/replace-spacing-lengths.ts index 14fad459e1e..5a6d892ad84 100644 --- a/polaris-migrator/src/migrations/replace-spacing-lengths/replace-spacing-lengths.ts +++ b/polaris-migrator/src/migrations/replace-spacing-lengths/replace-spacing-lengths.ts @@ -1,8 +1,6 @@ import valueParser from 'postcss-value-parser'; -import {POLARIS_MIGRATOR_COMMENT} from '../../constants'; import { - createInlineComment, getFunctionArgs, isNumericOperator, isSassFunction, @@ -16,44 +14,42 @@ import {isKeyOf} from '../../utilities/type-guards'; export default createSassMigrator( 'replace-sass-space', - (_, options, context) => { + (_, {methods, options}, context) => { const namespacedRem = namespace('rem', options); return (root) => { root.walkDecls((decl) => { if (!spaceProps.has(decl.prop)) return; - /** - * A collection of transformable values to migrate (e.g. decl lengths, functions, etc.) - * - * Note: This is evaluated at the end of each visitor execution to determine whether - * or not to replace the declaration or insert a comment. - */ - const targets: {replaced: boolean}[] = []; - let hasNumericOperator = false; const parsedValue = valueParser(decl.value); handleSpaceProps(); - if (targets.some(({replaced}) => !replaced || hasNumericOperator)) { - decl.before( - createInlineComment(POLARIS_MIGRATOR_COMMENT, {prose: true}), - ); - decl.before( - createInlineComment(`${decl.prop}: ${parsedValue.toString()};`), - ); - } else if (context.fix) { - decl.value = parsedValue.toString(); + const newValue = parsedValue.toString(); + + if (context.fix && newValue !== decl.value) { + if (methods.getReportsForNode(decl)) { + // The "partial fix" case: When there's a new value AND a report. + methods.report({ + node: decl, + severity: 'suggestion', + message: `${decl.prop}: ${parsedValue.toString()}`, + }); + } else { + decl.value = parsedValue.toString(); + } } - // - // Handlers - // + methods.flushReports(); function handleSpaceProps() { parsedValue.walk((node) => { if (isNumericOperator(node)) { - hasNumericOperator = true; + methods.report({ + node: decl, + severity: 'warning', + message: 'Numeric operator detected.', + }); return; } @@ -64,41 +60,72 @@ export default createSassMigrator( if (!isTransformableLength(dimension)) return; - targets.push({replaced: false}); - const valueInPx = toTransformablePx(node.value); - if (!isKeyOf(spaceMap, valueInPx)) return; + if (!isKeyOf(spaceMap, valueInPx)) { + methods.report({ + node: decl, + severity: 'error', + message: `Non-tokenizable value '${node.value}'`, + }); + return; + } - targets[targets.length - 1]!.replaced = true; + if (context.fix) { + node.value = `var(${spaceMap[valueInPx]})`; + return; + } - node.value = `var(${spaceMap[valueInPx]})`; + methods.report({ + node: decl, + severity: 'error', + message: `Prefer var(${spaceMap[valueInPx]}) Polaris token.`, + }); return; } if (node.type === 'function') { if (isSassFunction(namespacedRem, node)) { - targets.push({replaced: false}); - const args = getFunctionArgs(node); - if (args.length !== 1) return; + if (args.length !== 1) { + methods.report({ + node: decl, + severity: 'error', + message: `Expected 1 argument, got ${args.length}`, + }); + return; + } const valueInPx = toTransformablePx(args[0]); - if (!isKeyOf(spaceMap, valueInPx)) return; - - targets[targets.length - 1]!.replaced = true; - - node.value = 'var'; - node.nodes = [ - { - type: 'word', - value: spaceMap[valueInPx], - sourceIndex: node.nodes[0]?.sourceIndex ?? 0, - sourceEndIndex: spaceMap[valueInPx].length, - }, - ]; + if (!isKeyOf(spaceMap, valueInPx)) { + methods.report({ + node: decl, + severity: 'error', + message: `Non-tokenizable value '${args[0].trim()}'`, + }); + return; + } + + if (context.fix) { + node.value = 'var'; + node.nodes = [ + { + type: 'word', + value: spaceMap[valueInPx], + sourceIndex: node.nodes[0]?.sourceIndex ?? 0, + sourceEndIndex: spaceMap[valueInPx].length, + }, + ]; + return; + } + + methods.report({ + node: decl, + severity: 'error', + message: `Prefer var(${spaceMap[valueInPx]}) Polaris token.`, + }); } return StopWalkingFunctionNodes; diff --git a/polaris-migrator/src/migrations/replace-spacing-lengths/tests/replace-spacing-lengths.output.scss b/polaris-migrator/src/migrations/replace-spacing-lengths/tests/replace-spacing-lengths.output.scss index d783142336b..becc4078125 100644 --- a/polaris-migrator/src/migrations/replace-spacing-lengths/tests/replace-spacing-lengths.output.scss +++ b/polaris-migrator/src/migrations/replace-spacing-lengths/tests/replace-spacing-lengths.output.scss @@ -15,6 +15,8 @@ padding: 0; padding: 1; padding: 2; + // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. padding: #{16px + 16px}; padding: layout-width(nav); padding: 10em; @@ -23,55 +25,65 @@ padding: var(--p-space-4, 16px); // Comment // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10px; + // error: Non-tokenizable value '10px' padding: 10px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10rem; + // error: Non-tokenizable value '10rem' padding: 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10px 10px; + // error: Non-tokenizable value '10px' padding: 10px 10px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10px' // padding: var(--p-space-4) 10px; padding: 16px 10px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10rem 10rem; + // error: Non-tokenizable value '10rem' padding: 10rem 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10rem' // padding: var(--p-space-4) 10rem; padding: 1rem 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10px 10rem; + // error: Non-tokenizable value '10px' + // error: Non-tokenizable value '10rem' padding: 10px 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10rem' // padding: var(--p-space-4) 10rem; padding: 16px 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10px' // padding: 10px var(--p-space-4); padding: 10px 1rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '-16px' // padding: var(--p-space-4) -16px; padding: 16px -16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: var(--p-space-4) + var(--p-space-4); padding: 16px + 16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: var(--p-space-4) + var(--p-space-4) var(--p-space-4); padding: 16px + 16px 16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: $var + var(--p-space-4); padding: $var + 16px; padding: calc(16px + 16px); // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: var(--p-space-4) + #{16px + 16px}; padding: 16px + #{16px + 16px}; // Skip negative lengths. Need to discuss replacement strategy e.g. // calc(-1 * var(--p-space-*)) vs var(--p-space--*) // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: -16px; + // error: Non-tokenizable value '-16px' padding: -16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: -10px; + // error: Non-tokenizable value '-10px' padding: -10px; // REM FUNCTION @@ -86,21 +98,26 @@ padding: calc(10rem + var(--p-choice-size, #{rem(10px)})); // Comment // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: rem(10px); + // error: Non-tokenizable value '10px' padding: rem(10px); // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: rem(10px) rem(10px); + // error: Non-tokenizable value '10px' padding: rem(10px) rem(10px); // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10px' // padding: var(--p-space-4) rem(10px); padding: rem(16px) rem(10px); // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '-16px' // padding: var(--p-space-4) -16px; padding: rem(16px) -16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: var(--p-space-4) + var(--p-space-4); padding: rem(16px) + 16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '$var \* 16px' + // warning: Numeric operator detected. // padding: rem($var * var(--p-space-4)); padding: rem($var * 16px); } diff --git a/polaris-migrator/src/migrations/replace-spacing-lengths/tests/with-namespace.output.scss b/polaris-migrator/src/migrations/replace-spacing-lengths/tests/with-namespace.output.scss index 6da935f98aa..2b9be603f01 100644 --- a/polaris-migrator/src/migrations/replace-spacing-lengths/tests/with-namespace.output.scss +++ b/polaris-migrator/src/migrations/replace-spacing-lengths/tests/with-namespace.output.scss @@ -17,6 +17,8 @@ padding: 0; padding: 1; padding: 2; + // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. padding: #{16px + 16px}; padding: layout-width(nav); padding: 10em; @@ -25,55 +27,65 @@ padding: var(--p-space-4, 16px); // Comment // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10px; + // error: Non-tokenizable value '10px' padding: 10px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10rem; + // error: Non-tokenizable value '10rem' padding: 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10px 10px; + // error: Non-tokenizable value '10px' padding: 10px 10px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10px' // padding: var(--p-space-4) 10px; padding: 16px 10px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10rem 10rem; + // error: Non-tokenizable value '10rem' padding: 10rem 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10rem' // padding: var(--p-space-4) 10rem; padding: 1rem 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: 10px 10rem; + // error: Non-tokenizable value '10px' + // error: Non-tokenizable value '10rem' padding: 10px 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10rem' // padding: var(--p-space-4) 10rem; padding: 16px 10rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10px' // padding: 10px var(--p-space-4); padding: 10px 1rem; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '-16px' // padding: var(--p-space-4) -16px; padding: 16px -16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: var(--p-space-4) + var(--p-space-4); padding: 16px + 16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: var(--p-space-4) + var(--p-space-4) var(--p-space-4); padding: 16px + 16px 16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: $var + var(--p-space-4); padding: $var + 16px; padding: calc(16px + 16px); // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: var(--p-space-4) + #{16px + 16px}; padding: 16px + #{16px + 16px}; // Skip negative lengths. Need to discuss replacement strategy e.g. // calc(-1 * var(--p-space-*)) vs var(--p-space--*) // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: -16px; + // error: Non-tokenizable value '-16px' padding: -16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: -10px; + // error: Non-tokenizable value '-10px' padding: -10px; // REM FUNCTION @@ -88,21 +100,26 @@ padding: calc(10rem + var(--p-choice-size, #{legacy-polaris-v8.rem(10px)})); // Comment // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: legacy-polaris-v8.rem(10px); + // error: Non-tokenizable value '10px' padding: legacy-polaris-v8.rem(10px); // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. - // padding: legacy-polaris-v8.rem(10px) legacy-polaris-v8.rem(10px); + // error: Non-tokenizable value '10px' padding: legacy-polaris-v8.rem(10px) legacy-polaris-v8.rem(10px); // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '10px' // padding: var(--p-space-4) legacy-polaris-v8.rem(10px); padding: legacy-polaris-v8.rem(16px) legacy-polaris-v8.rem(10px); // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '-16px' // padding: var(--p-space-4) -16px; padding: legacy-polaris-v8.rem(16px) -16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // warning: Numeric operator detected. // padding: var(--p-space-4) + var(--p-space-4); padding: legacy-polaris-v8.rem(16px) + 16px; // polaris-migrator: Unable to migrate the following expression. Please upgrade manually. + // error: Non-tokenizable value '$var \* 16px' + // warning: Numeric operator detected. // padding: legacy-polaris-v8.rem($var * var(--p-space-4)); padding: legacy-polaris-v8.rem($var * 16px); } diff --git a/polaris-migrator/src/utilities/sass.ts b/polaris-migrator/src/utilities/sass.ts index f1130f58062..a9e61bc1e0e 100644 --- a/polaris-migrator/src/utilities/sass.ts +++ b/polaris-migrator/src/utilities/sass.ts @@ -1,5 +1,5 @@ import type {FileInfo, API, Options} from 'jscodeshift'; -import postcss, {Root, Result, Plugin} from 'postcss'; +import postcss, {Root, Result, Plugin, Node as PostCSSNode} from 'postcss'; import valueParser, { Node, ParsedValue, @@ -9,6 +9,8 @@ import valueParser, { import {toPx} from '@shopify/polaris-tokens'; import prettier from 'prettier'; +import {POLARIS_MIGRATOR_COMMENT} from '../constants'; + const defaultNamespace = ''; function getNamespace(options?: NamespaceOptions) { @@ -255,6 +257,12 @@ export function createInlineComment(text: string, options?: {prose?: boolean}) { interface PluginOptions extends Options, NamespaceOptions {} +interface Report { + node: PostCSSNode; + severity: 'warning' | 'error' | 'suggestion'; + message: string; +} + interface PluginContext { fix: boolean; } @@ -280,7 +288,14 @@ type StylelintRule

= StylelintRuleBase & { export type PolarisMigrator = ( primaryOption: true, - secondaryOptions: PluginOptions, + secondaryOptions: { + options: {[key: string]: any}; + methods: { + report: (report: Report) => void; + flushReports: () => void; + getReportsForNode: (node: PostCSSNode) => Report[] | undefined; + }; + }, context: PluginContext, ) => (root: Root, result: Result) => void; @@ -327,7 +342,70 @@ function convertStylelintRuleToPostcssProcessor(ruleFn: StylelintRule) { } export function createSassMigrator(name: string, ruleFn: PolarisMigrator) { - const wrappedRule = ruleFn as StylelintRule; + const wrappedRule: StylelintRule = (( + primary, + secondaryOptions: PluginOptions, + context, + ) => { + const reports = new Map(); + + const addDedupedReport = (newReport: Report) => { + if (!reports.has(newReport.node)) { + reports.set(newReport.node, []); + } + + const reportsForNode = reports.get(newReport.node)!; + + if ( + reportsForNode.findIndex( + (existingReport) => + existingReport.severity === newReport.severity && + existingReport.message === newReport.message, + ) === -1 + ) { + reportsForNode.push(newReport); + } + }; + + const flushReportsAsComments = () => { + // @ts-expect-error No idea why TS is complaining here + for (const [node, reportsForNode] of reports) { + node.before( + createInlineComment(POLARIS_MIGRATOR_COMMENT, {prose: true}), + ); + + for (const report of reportsForNode) { + node.before( + report.severity === 'suggestion' + ? createInlineComment(report.message) + : createInlineComment(`${report.severity}: ${report.message}`, { + prose: true, + }), + ); + } + } + reports.clear(); + }; + + const getReportsForNode = (node: PostCSSNode) => reports.get(node); + + return ruleFn( + primary, + // We're kind of abusing stylelint's types here since the + // SecondaryOptions param can take an arbitrary object. But we need a + // way to pass the methods into the rule function somehow, and this way + // means less Typescript hackery. + { + options: secondaryOptions, + methods: { + report: addDedupedReport, + flushReports: flushReportsAsComments, + getReportsForNode, + }, + }, + context, + ); + }) as StylelintRule; wrappedRule.ruleName = name; wrappedRule.meta = { diff --git a/polaris-migrator/templates/sass-migration/{{kebabCase migrationName}}/{{kebabCase migrationName}}.ts.hbs b/polaris-migrator/templates/sass-migration/{{kebabCase migrationName}}/{{kebabCase migrationName}}.ts.hbs index f71d25a40b7..488c77a7c19 100644 --- a/polaris-migrator/templates/sass-migration/{{kebabCase migrationName}}/{{kebabCase migrationName}}.ts.hbs +++ b/polaris-migrator/templates/sass-migration/{{kebabCase migrationName}}/{{kebabCase migrationName}}.ts.hbs @@ -5,16 +5,39 @@ import { StopWalkingFunctionNodes, createSassMigrator, } from '../../utilities/sass'; +import type {PolarisMigrator} from '../../utilities/sass'; -// options can be passed in from cli / config. -export default createSassMigrator('{{kebabCase migrationName}}', (_, options, context) => { +const {{camelCase migrationName}}: PolarisMigrator = ( + _, + // options will be passed in from cli / config. + {methods /* , options */}, + // Use context.fix to change behaviour based on if the user has passed the + // `--fix` flag (always true for `polaris-migrator` CLI). + context, +) => { return (root) => { root.walkDecls((decl) => { + // Using the parsedValue allows easy detection of individual functions and + // properties. Particularly useful when dealing with shorthand + // declarations such as `border`, `padding`, etc. const parsedValue = valueParser(decl.value); - parsedValue.walk((node) => { if (isSassFunction('hello', node)) { - node.value = 'world'; + if (context.fix) { + // When fixing, we mutate the node directly. + node.value = 'world'; + } else { + // When not fixing, emit a report which will be aggregated and shown + // to the user once all migrations are run. + methods.report({ + node: decl, + severity: 'error', + message: + 'Method hello() is no longer supported. Please migrate to world().', + }); + } + + // We do not want to recursively walk the function's arguments. return StopWalkingFunctionNodes; } }); @@ -22,6 +45,14 @@ export default createSassMigrator('{{kebabCase migrationName}}', (_, options, co if (context.fix) { decl.value = parsedValue.toString(); } + + // Ensure all generated reports are flushed to the appropriate output + methods.flushReports(); }); }; -}); +}; + +export default createSassMigrator( + '{{kebabCase migrationName}}', + {{camelCase migrationName}} +);