From a85c03493d3e80e7bcb554489b6145d1f5fae917 Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Thu, 28 May 2026 11:56:31 -0300 Subject: [PATCH 1/9] Add code mods for Vue and Nuxt --- package-lock.json | 26 +- .../javascript/nuxtConfigModuleCodemod.ts | 221 ++++++++++ .../javascript/nuxtStoryblokPluginCodemod.ts | 107 +++++ .../javascript/utils/createJsxProps.ts | 81 +--- .../javascript/utils/createObjectProps.ts | 91 ++++ .../javascript/vuePluginCodemod.ts | 400 ++++++++++++++++++ .../javascript/vueStoryblokCodemod.ts | 121 ++++++ .../aliasedDefineNuxtConfig.ts | 5 + .../aliasedIndirectConfig.ts | 7 + .../nuxt-config-module/alreadyConfigured.ts | 3 + .../alreadyConfiguredAsTuple.ts | 5 + .../nuxt-config-module/bareObjectExport.ts | 3 + .../bareObjectExportNoModules.ts | 1 + .../bareObjectIndirectVariable.ts | 5 + .../defineCallWithReference.ts | 3 + .../nuxt-config-module/emptyDefineCall.ts | 1 + .../nuxt-config-module/emptyModulesArray.ts | 3 + .../nuxt-config-module/existingModules.ts | 3 + .../functionDefaultExport.ts | 3 + .../indirectConfigVariable.ts | 5 + .../indirectConfigWithExtraDeclarator.ts | 7 + .../nuxt-config-module/modulesAsSpread.ts | 5 + .../nuxt-config-module/noModulesKey.ts | 1 + .../nuxt-config-module/nonArrayModules.ts | 5 + .../nuxt-config-module/nonObjectExport.ts | 1 + .../nuxt-config-module/sparseModulesArray.ts | 3 + .../spreadConfigProperties.ts | 5 + .../stringLiteralModulesKey.ts | 3 + .../uninitializedBinding.ts | 3 + .../nuxt-config-module/withStoryblokModule.ts | 5 + .../withTypeAssertionAsConst.ts | 3 + .../withTypeAssertionSatisfies.ts | 5 + .../aliasedDefineNuxtPlugin.ts | 5 + .../alreadyScaffolded.ts | 7 + .../fixtures/nuxt-storyblok-plugin/empty.ts | 0 .../nuxt-storyblok-plugin/partialScaffold.ts | 5 + .../nuxt-storyblok-plugin/withComments.ts | 2 + .../vue-plugin/aliasedFactoryImportAndUsed.ts | 7 + .../fixtures/vue-plugin/alreadyRegistered.ts | 7 + .../fixtures/vue-plugin/argsReference.ts | 5 + .../fixtures/vue-plugin/argsTernary.ts | 5 + .../vue-plugin/chainedAlreadyRegistered.ts | 5 + .../fixtures/vue-plugin/chainedMount.ts | 4 + .../fixtures/vue-plugin/chainedWithPlugins.ts | 6 + .../vue-plugin/computedMountAccess.ts | 6 + .../vue-plugin/createAppAsArgument.ts | 4 + .../vue-plugin/createAppFromOtherPackage.ts | 3 + .../createAppImportedButNotCalled.ts | 3 + .../vue-plugin/destructuredCreateApp.ts | 7 + .../fixtures/vue-plugin/exportedCreateApp.ts | 6 + .../vue-plugin/importedAliasedButNotUsed.ts | 6 + .../fixtures/vue-plugin/importedButNotUsed.ts | 6 + .../fixtures/vue-plugin/incompleteChain.ts | 5 + .../vue-plugin/intermediateNonUseStatement.ts | 8 + .../vue-plugin/intermediateUnrelatedCalls.ts | 11 + .../vue-plugin/mountWithDocumentBody.ts | 5 + .../multipleCreateAppCallsChainedFirst.ts | 8 + .../multipleCreateAppCallsVariableFirst.ts | 8 + .../fixtures/vue-plugin/noArgs.ts | 5 + .../fixtures/vue-plugin/noCreateAppCall.ts | 3 + .../fixtures/vue-plugin/noMountCall.ts | 5 + .../fixtures/vue-plugin/typescriptInput.ts | 5 + .../fixtures/vue-plugin/variableForm.ts | 5 + .../vue-plugin/variableWithPlugins.ts | 9 + .../vue-storyblok/storyblokAliasedImport.ts | 10 + .../vue-storyblok/storyblokAlreadyMigrated.ts | 11 + .../vue-storyblok/storyblokBasicMigration.ts | 11 + .../vue-storyblok/storyblokChainedAppUse.ts | 10 + .../storyblokImportedButNoUseCall.ts | 8 + .../storyblokMultipleAppUseCalls.ts | 14 + .../vue-storyblok/storyblokNoCreateApp.ts | 6 + .../vue-storyblok/storyblokNoImport.ts | 6 + .../vue-storyblok/storyblokOnlySpecifier.ts | 9 + .../storyblokOptionsArePreserved.ts | 15 + .../vue-storyblok/storyblokStringSpecifier.ts | 10 + .../storyblokWithCroctAliasedButNotUsed.ts | 11 + .../storyblokWithCroctImportedButNotUsed.ts | 11 + .../storyblokWithDefaultSpecifier.ts | 12 + .../storyblokWithOtherSpecifiers.ts | 12 + .../nuxtConfigModuleCodemod.test.ts.snap | 200 +++++++++ .../nuxtStoryblokPluginCodemod.test.ts.snap | 51 +++ .../vuePluginCodemod.test.ts.snap | 332 +++++++++++++++ .../vueStoryblokCodemod.test.ts.snap | 229 ++++++++++ .../nuxtConfigModuleCodemod.test.ts | 30 ++ .../nuxtStoryblokPluginCodemod.test.ts | 37 ++ .../utils/createObjectProps.test.ts | 180 ++++++++ .../javascript/vuePluginCodemod.test.ts | 213 ++++++++++ .../javascript/vueStoryblokCodemod.test.ts | 35 ++ 88 files changed, 2686 insertions(+), 93 deletions(-) create mode 100644 src/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.ts create mode 100644 src/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.ts create mode 100644 src/application/project/code/transformation/javascript/utils/createObjectProps.ts create mode 100644 src/application/project/code/transformation/javascript/vuePluginCodemod.ts create mode 100644 src/application/project/code/transformation/javascript/vueStoryblokCodemod.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/aliasedDefineNuxtConfig.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/aliasedIndirectConfig.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/alreadyConfigured.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/alreadyConfiguredAsTuple.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectExport.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectExportNoModules.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectIndirectVariable.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/defineCallWithReference.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/emptyDefineCall.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/emptyModulesArray.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/existingModules.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/functionDefaultExport.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/indirectConfigVariable.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/indirectConfigWithExtraDeclarator.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/modulesAsSpread.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/noModulesKey.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/nonArrayModules.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/nonObjectExport.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/sparseModulesArray.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/spreadConfigProperties.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/stringLiteralModulesKey.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/uninitializedBinding.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/withStoryblokModule.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/withTypeAssertionAsConst.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config-module/withTypeAssertionSatisfies.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/aliasedDefineNuxtPlugin.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/alreadyScaffolded.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/empty.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/partialScaffold.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/withComments.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/aliasedFactoryImportAndUsed.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/alreadyRegistered.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/argsReference.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/argsTernary.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/chainedAlreadyRegistered.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/chainedMount.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/chainedWithPlugins.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/computedMountAccess.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/createAppAsArgument.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/createAppFromOtherPackage.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/createAppImportedButNotCalled.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/destructuredCreateApp.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/exportedCreateApp.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/importedAliasedButNotUsed.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/importedButNotUsed.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/incompleteChain.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/intermediateNonUseStatement.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/intermediateUnrelatedCalls.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/mountWithDocumentBody.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/multipleCreateAppCallsChainedFirst.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/multipleCreateAppCallsVariableFirst.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/noArgs.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/noCreateAppCall.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/noMountCall.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/typescriptInput.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/variableForm.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/variableWithPlugins.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokAliasedImport.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokAlreadyMigrated.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokBasicMigration.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokChainedAppUse.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokImportedButNoUseCall.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokMultipleAppUseCalls.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokNoCreateApp.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokNoImport.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokOnlySpecifier.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokOptionsArePreserved.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokStringSpecifier.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithCroctAliasedButNotUsed.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithCroctImportedButNotUsed.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithDefaultSpecifier.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithOtherSpecifiers.ts create mode 100644 test/application/project/code/transformation/javascript/__snapshots__/nuxtConfigModuleCodemod.test.ts.snap create mode 100644 test/application/project/code/transformation/javascript/__snapshots__/nuxtStoryblokPluginCodemod.test.ts.snap create mode 100644 test/application/project/code/transformation/javascript/__snapshots__/vuePluginCodemod.test.ts.snap create mode 100644 test/application/project/code/transformation/javascript/__snapshots__/vueStoryblokCodemod.test.ts.snap create mode 100644 test/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.test.ts create mode 100644 test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts create mode 100644 test/application/project/code/transformation/javascript/utils/createObjectProps.test.ts create mode 100644 test/application/project/code/transformation/javascript/vuePluginCodemod.test.ts create mode 100644 test/application/project/code/transformation/javascript/vueStoryblokCodemod.test.ts diff --git a/package-lock.json b/package-lock.json index 4dbf7d68..4d14550d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,7 +159,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -4745,6 +4744,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -4762,6 +4762,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -4779,6 +4780,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4796,6 +4798,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4813,6 +4816,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4830,6 +4834,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4847,6 +4852,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -4864,6 +4870,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -4881,6 +4888,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -4898,6 +4906,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -4933,6 +4942,7 @@ "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3" } @@ -5094,7 +5104,6 @@ "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5167,7 +5176,6 @@ "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", @@ -5197,7 +5205,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -5373,7 +5380,6 @@ "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", @@ -5742,7 +5748,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6472,7 +6477,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7084,7 +7088,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -7872,7 +7875,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7947,7 +7949,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10529,7 +10530,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -12547,7 +12547,6 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -15979,7 +15978,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16124,7 +16122,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -16492,7 +16489,6 @@ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/src/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.ts b/src/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.ts new file mode 100644 index 00000000..7053153d --- /dev/null +++ b/src/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.ts @@ -0,0 +1,221 @@ +import * as t from '@babel/types'; +import {traverse} from '@babel/core'; +import type {Codemod, CodemodOptions, ResultCode} from '@/application/project/code/transformation/codemod'; +import {getImportLocalName} from '@/application/project/code/transformation/javascript/utils/getImportLocalName'; + +export type NuxtConfigModuleConfiguration = { + moduleName: string, +}; + +/** + * Appends a module to the modules array of the Nuxt configuration. + * + * Supports both the defineNuxtConfig call form and bare object exports, + * including the indirect variable forms of each. Type-assertion wrappers + * are transparently unwrapped. If the module is already configured, the + * codemod returns unmodified. + */ +export class NuxtConfigModuleCodemod implements Codemod { + private readonly configuration: NuxtConfigModuleConfiguration; + + public constructor(configuration: NuxtConfigModuleConfiguration) { + this.configuration = configuration; + } + + public apply(input: t.File): Promise> { + const config = NuxtConfigModuleCodemod.findConfig(input); + + if (config === null) { + return Promise.resolve({modified: false, result: input}); + } + + const modulesProperty = NuxtConfigModuleCodemod.findModulesProperty(config); + + if (modulesProperty === null) { + config.properties.push( + t.objectProperty( + t.identifier('modules'), + t.arrayExpression([t.stringLiteral(this.configuration.moduleName)]), + ), + ); + + return Promise.resolve({modified: true, result: input}); + } + + if (!t.isArrayExpression(modulesProperty.value)) { + return Promise.resolve({modified: false, result: input}); + } + + if (this.hasModule(modulesProperty.value)) { + return Promise.resolve({modified: false, result: input}); + } + + modulesProperty.value + .elements + .push(t.stringLiteral(this.configuration.moduleName)); + + return Promise.resolve({modified: true, result: input}); + } + + private hasModule(array: t.ArrayExpression): boolean { + for (const element of array.elements) { + if (element === null) { + continue; + } + + if (t.isStringLiteral(element) && element.value === this.configuration.moduleName) { + return true; + } + + if (t.isArrayExpression(element)) { + const [first] = element.elements; + + if (first !== null && t.isStringLiteral(first) && first.value === this.configuration.moduleName) { + return true; + } + } + } + + return false; + } + + private static findConfig(ast: t.File): t.ObjectExpression | null { + const defineName = NuxtConfigModuleCodemod.resolveDefineName(ast); + + let configObject: t.ObjectExpression | null = null; + + traverse(ast, { + ExportDefaultDeclaration: path => { + const declaration = NuxtConfigModuleCodemod.unwrapTypeWrapper(path.node.declaration); + + if (t.isObjectExpression(declaration)) { + configObject = declaration; + + return path.stop(); + } + + if (t.isCallExpression(declaration)) { + const inner = NuxtConfigModuleCodemod.unwrapDefineCall(declaration, defineName); + + if (inner !== null) { + configObject = inner; + + return path.stop(); + } + + return path.skip(); + } + + if (t.isIdentifier(declaration)) { + const resolved = NuxtConfigModuleCodemod.resolveBinding(ast, declaration.name, defineName); + + if (resolved !== null) { + configObject = resolved; + } + + return path.stop(); + } + + return path.skip(); + }, + }); + + return configObject; + } + + private static resolveDefineName(ast: t.File): string { + return getImportLocalName(ast, { + moduleName: /^nuxt(\/.+)?$/, + importName: 'defineNuxtConfig', + }) ?? 'defineNuxtConfig'; + } + + private static findModulesProperty(config: t.ObjectExpression): t.ObjectProperty | null { + for (const property of config.properties) { + if (!t.isObjectProperty(property) || property.computed) { + continue; + } + + const {key} = property; + + if (t.isIdentifier(key) && key.name === 'modules') { + return property; + } + + if (t.isStringLiteral(key) && key.value === 'modules') { + return property; + } + } + + return null; + } + + private static unwrapDefineCall(call: t.CallExpression, defineName: string): t.ObjectExpression | null { + if (!t.isIdentifier(call.callee) || call.callee.name !== defineName) { + return null; + } + + const firstArg = call.arguments[0]; + + if (firstArg === undefined) { + return null; + } + + const unwrapped = NuxtConfigModuleCodemod.unwrapTypeWrapper(firstArg); + + return t.isObjectExpression(unwrapped) ? unwrapped : null; + } + + private static unwrapTypeWrapper(node: t.Node): t.Node { + let current = node; + + while ( + t.isTSAsExpression(current) + || t.isTSSatisfiesExpression(current) + || t.isTSTypeAssertion(current) + || t.isTSNonNullExpression(current) + ) { + current = current.expression; + } + + return current; + } + + private static resolveBinding(ast: t.File, name: string, defineName: string): t.ObjectExpression | null { + let resolved: t.ObjectExpression | null = null; + + traverse(ast, { + VariableDeclarator: path => { + const {node} = path; + + if (!t.isIdentifier(node.id) || node.id.name !== name) { + return; + } + + if (node.init === undefined || node.init === null) { + return; + } + + const init = NuxtConfigModuleCodemod.unwrapTypeWrapper(node.init); + + if (t.isObjectExpression(init)) { + resolved = init; + + return path.stop(); + } + + if (t.isCallExpression(init)) { + const inner = NuxtConfigModuleCodemod.unwrapDefineCall(init, defineName); + + if (inner !== null) { + resolved = inner; + + return path.stop(); + } + } + }, + }); + + return resolved; + } +} diff --git a/src/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.ts b/src/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.ts new file mode 100644 index 00000000..4902a451 --- /dev/null +++ b/src/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.ts @@ -0,0 +1,107 @@ +import * as t from '@babel/types'; +import {traverse} from '@babel/core'; +import type {Codemod, CodemodOptions, ResultCode} from '@/application/project/code/transformation/codemod'; +import {addImport} from '@/application/project/code/transformation/javascript/utils/addImport'; +import {getImportLocalName} from '@/application/project/code/transformation/javascript/utils/getImportLocalName'; + +export type NuxtStoryblokPluginConfiguration = { + plugin: { + module: string, + factory: string, + }, + storyblokVueModule: string, + nuxtAppModule: string, +}; + +/** + * Scaffolds the Croct Storyblok plugin file for Nuxt. + * + * Generates the canonical plugin body that wires the Storyblok API into Croct + * inside a defineNuxtPlugin callback. Leaves hand-edited files untouched: if a + * plugin definition is already present, the codemod returns unmodified. + */ +export class NuxtStoryblokPluginCodemod implements Codemod { + private readonly configuration: NuxtStoryblokPluginConfiguration; + + public constructor(configuration: NuxtStoryblokPluginConfiguration) { + this.configuration = configuration; + } + + public apply(input: t.File): Promise> { + if (NuxtStoryblokPluginCodemod.hasPluginDefinition(input)) { + return Promise.resolve({modified: false, result: input}); + } + + const factoryImport = addImport(input, { + type: 'value', + moduleName: this.configuration.plugin.module, + importName: this.configuration.plugin.factory, + }); + + const useStoryblokApiImport = addImport(input, { + type: 'value', + moduleName: this.configuration.storyblokVueModule, + importName: 'useStoryblokApi', + }); + + const defineNuxtPluginImport = addImport(input, { + type: 'value', + moduleName: this.configuration.nuxtAppModule, + importName: 'defineNuxtPlugin', + }); + + const pluginCallback = t.arrowFunctionExpression( + [t.identifier('nuxtApp')], + t.blockStatement([ + t.expressionStatement( + t.callExpression( + t.identifier(factoryImport.localName), + [ + t.identifier('nuxtApp'), + t.callExpression( + t.identifier(useStoryblokApiImport.localName), + [], + ), + ], + ), + ), + ]), + ); + + const defaultExport = t.exportDefaultDeclaration( + t.callExpression( + t.identifier(defineNuxtPluginImport.localName), + [pluginCallback], + ), + ); + + input.program + .body + .push(defaultExport); + + return Promise.resolve({modified: true, result: input}); + } + + private static hasPluginDefinition(ast: t.File): boolean { + const defineName = getImportLocalName(ast, { + moduleName: /^(#app|nuxt(\/.+)?)$/, + importName: 'defineNuxtPlugin', + }) ?? 'defineNuxtPlugin'; + + let found = false; + + traverse(ast, { + CallExpression: path => { + const {callee} = path.node; + + if (t.isIdentifier(callee) && callee.name === defineName) { + found = true; + + path.stop(); + } + }, + }); + + return found; + } +} diff --git a/src/application/project/code/transformation/javascript/utils/createJsxProps.ts b/src/application/project/code/transformation/javascript/utils/createJsxProps.ts index 54de21fe..9e74dffe 100644 --- a/src/application/project/code/transformation/javascript/utils/createJsxProps.ts +++ b/src/application/project/code/transformation/javascript/utils/createJsxProps.ts @@ -1,31 +1,8 @@ import * as t from '@babel/types'; +import {buildPropertyExpression} from '@/application/project/code/transformation/javascript/utils/createObjectProps'; +import type {AttributeType} from '@/application/project/code/transformation/javascript/utils/createObjectProps'; -type PropertyTypes = { - reference: { - path: string[], - }, - literal: { - value: string | number | boolean | null, - }, - comparison: { - left: AttributeType, - operator: '===' | '!==' | '>' | '>=' | '<' | '<=', - right: AttributeType, - }, - ternary: { - condition: { - left: AttributeType, - operator: '===' | '!==' | '>' | '>=' | '<' | '<=', - right: AttributeType, - }, - consequent: AttributeType, - alternate: AttributeType, - }, -}; - -export type AttributeType = { - [K in keyof PropertyTypes]: PropertyTypes[K] & {type: K} -}[keyof PropertyTypes]; +export type {AttributeType} from '@/application/project/code/transformation/javascript/utils/createObjectProps'; export function createJsxAttributes(attributes: Record): t.JSXAttribute[] { const nodes: t.JSXAttribute[] = []; @@ -41,55 +18,3 @@ export function createJsxAttributes(attributes: Record): return nodes; } - -function buildPropertyExpression(attribute: AttributeType): t.Expression { - switch (attribute.type) { - case 'reference': - if (attribute.path.length < 2) { - return t.identifier(attribute.path[0]); - } - - return attribute.path - .slice(2) - .reduce( - (object, key) => t.memberExpression(object, t.identifier(key)), - t.memberExpression( - t.identifier(attribute.path[0]), - t.identifier(attribute.path[1]), - ), - ); - - case 'literal': - if (typeof attribute.value === 'string') { - return t.stringLiteral(attribute.value); - } - - if (typeof attribute.value === 'number') { - return t.numericLiteral(attribute.value); - } - - if (typeof attribute.value === 'boolean') { - return t.booleanLiteral(attribute.value); - } - - return t.nullLiteral(); - - case 'comparison': - return t.binaryExpression( - attribute.operator, - buildPropertyExpression(attribute.left), - buildPropertyExpression(attribute.right), - ); - - case 'ternary': - return t.conditionalExpression( - t.binaryExpression( - attribute.condition.operator, - buildPropertyExpression(attribute.condition.left), - buildPropertyExpression(attribute.condition.right), - ), - buildPropertyExpression(attribute.consequent), - buildPropertyExpression(attribute.alternate), - ); - } -} diff --git a/src/application/project/code/transformation/javascript/utils/createObjectProps.ts b/src/application/project/code/transformation/javascript/utils/createObjectProps.ts new file mode 100644 index 00000000..4ad07915 --- /dev/null +++ b/src/application/project/code/transformation/javascript/utils/createObjectProps.ts @@ -0,0 +1,91 @@ +import * as t from '@babel/types'; + +type PropertyTypes = { + reference: { + path: string[], + }, + literal: { + value: string | number | boolean | null, + }, + comparison: { + left: AttributeType, + operator: '===' | '!==' | '>' | '>=' | '<' | '<=', + right: AttributeType, + }, + ternary: { + condition: { + left: AttributeType, + operator: '===' | '!==' | '>' | '>=' | '<' | '<=', + right: AttributeType, + }, + consequent: AttributeType, + alternate: AttributeType, + }, +}; + +export type AttributeType = { + [K in keyof PropertyTypes]: PropertyTypes[K] & {type: K} +}[keyof PropertyTypes]; + +export function buildPropertyExpression(attribute: AttributeType): t.Expression { + switch (attribute.type) { + case 'reference': + if (attribute.path.length < 2) { + return t.identifier(attribute.path[0]); + } + + return attribute.path + .slice(2) + .reduce( + (object, key) => t.memberExpression(object, t.identifier(key)), + t.memberExpression( + t.identifier(attribute.path[0]), + t.identifier(attribute.path[1]), + ), + ); + + case 'literal': + if (typeof attribute.value === 'string') { + return t.stringLiteral(attribute.value); + } + + if (typeof attribute.value === 'number') { + return t.numericLiteral(attribute.value); + } + + if (typeof attribute.value === 'boolean') { + return t.booleanLiteral(attribute.value); + } + + return t.nullLiteral(); + + case 'comparison': + return t.binaryExpression( + attribute.operator, + buildPropertyExpression(attribute.left), + buildPropertyExpression(attribute.right), + ); + + case 'ternary': + return t.conditionalExpression( + t.binaryExpression( + attribute.condition.operator, + buildPropertyExpression(attribute.condition.left), + buildPropertyExpression(attribute.condition.right), + ), + buildPropertyExpression(attribute.consequent), + buildPropertyExpression(attribute.alternate), + ); + } +} + +export function createPropsObject(attributes: Record): t.ObjectExpression { + return t.objectExpression( + Object.entries(attributes).map( + ([key, value]) => t.objectProperty( + t.identifier(key), + buildPropertyExpression(value), + ), + ), + ); +} diff --git a/src/application/project/code/transformation/javascript/vuePluginCodemod.ts b/src/application/project/code/transformation/javascript/vuePluginCodemod.ts new file mode 100644 index 00000000..b03f7102 --- /dev/null +++ b/src/application/project/code/transformation/javascript/vuePluginCodemod.ts @@ -0,0 +1,400 @@ +import * as t from '@babel/types'; +import {traverse} from '@babel/core'; +import type {Codemod, CodemodOptions, ResultCode} from '@/application/project/code/transformation/codemod'; +import {addImport} from '@/application/project/code/transformation/javascript/utils/addImport'; +import {getImportLocalName} from '@/application/project/code/transformation/javascript/utils/getImportLocalName'; +import type {AttributeType} from '@/application/project/code/transformation/javascript/utils/createObjectProps'; +import {createPropsObject} from '@/application/project/code/transformation/javascript/utils/createObjectProps'; + +type ChainCall = t.CallExpression & {callee: t.MemberExpression}; + +type ChainAnchor = { + kind: 'chain', + mountCall: ChainCall, +}; + +type VariableAnchor = { + kind: 'variable', + binding: string, + parentBody: t.Statement[], + declarationStatement: t.Node, + mountStatement: t.ExpressionStatement, +}; + +export type MountAnchor = ChainAnchor | VariableAnchor; + +type CreateAppMatch = { + call: t.CallExpression, + parents: t.Node[], +}; + +export type VuePluginConfiguration = { + plugin: { + module: string, + factory: string, + }, + args?: Record, +}; + +/** + * Registers a Vue plugin in the main entry file. + * + * Inserts the plugin registration just before app.mount, preserving whichever + * form the user wrote (chained or variable). If the plugin is already + * registered, the codemod returns unmodified. + */ +export class VuePluginCodemod implements Codemod { + private readonly configuration: VuePluginConfiguration; + + public constructor(configuration: VuePluginConfiguration) { + this.configuration = configuration; + } + + public apply(input: t.File): Promise> { + const anchor = VuePluginCodemod.findMountAnchor(input); + + if (anchor === null) { + return Promise.resolve({modified: false, result: input}); + } + + const {plugin, args} = this.configuration; + + const factoryLocal = getImportLocalName(input, { + moduleName: plugin.module, + importName: plugin.factory, + }); + + if (factoryLocal !== null && VuePluginCodemod.hasFactoryCall(anchor, factoryLocal)) { + return Promise.resolve({modified: false, result: input}); + } + + const factoryImport = addImport(input, { + type: 'value', + moduleName: plugin.module, + importName: plugin.factory, + }); + + const factoryCall = t.callExpression( + t.identifier(factoryImport.localName), + args === undefined ? [] : [createPropsObject(args)], + ); + + if (anchor.kind === 'chain') { + VuePluginCodemod.injectIntoChain(anchor, factoryCall); + } else { + VuePluginCodemod.injectBeforeMount(anchor, factoryCall); + } + + return Promise.resolve({modified: true, result: input}); + } + + public static findMountAnchor(ast: t.File): MountAnchor | null { + const createAppName = getImportLocalName(ast, { + moduleName: 'vue', + importName: 'createApp', + }); + + if (createAppName === null) { + return null; + } + + const match = VuePluginCodemod.findCreateAppCall(ast, createAppName); + + if (match === null) { + return null; + } + + return VuePluginCodemod.matchChainAnchor(match) ?? VuePluginCodemod.matchVariableAnchor(match); + } + + public static hasFactoryCall(anchor: MountAnchor, factoryLocal: string): boolean { + return anchor.kind === 'chain' + ? VuePluginCodemod.chainHasFactoryCall(anchor, factoryLocal) + : VuePluginCodemod.variableBodyHasFactoryCall(anchor, factoryLocal); + } + + public static findUseCall(anchor: MountAnchor, identifier: string): t.CallExpression | null { + return anchor.kind === 'chain' + ? VuePluginCodemod.findUseCallInChain(anchor, identifier) + : VuePluginCodemod.findUseCallInBody(anchor, identifier); + } + + private static findCreateAppCall(ast: t.File, createAppName: string): CreateAppMatch | null { + let match: CreateAppMatch | null = null; + + traverse(ast, { + CallExpression: path => { + const {node} = path; + + if (!t.isIdentifier(node.callee) || node.callee.name !== createAppName) { + return; + } + + const parents: t.Node[] = []; + let current: typeof path.parentPath | null = path.parentPath; + + while (current !== null) { + parents.push(current.node); + current = current.parentPath; + } + + match = { + call: node, + parents: parents, + }; + + path.stop(); + }, + }); + + return match; + } + + private static matchChainAnchor(match: CreateAppMatch): ChainAnchor | null { + return VuePluginCodemod.walkChain(match.call, match.parents, 0); + } + + private static walkChain(inner: t.Node, parents: t.Node[], index: number): ChainAnchor | null { + const memberNode = parents[index]; + + if ( + memberNode === undefined + || !t.isMemberExpression(memberNode) + || memberNode.object !== inner + || memberNode.computed + || !t.isIdentifier(memberNode.property) + ) { + return null; + } + + const callNode = parents[index + 1]; + + if ( + callNode === undefined + || !VuePluginCodemod.isChainCall(callNode) + || callNode.callee !== memberNode + ) { + return null; + } + + if (memberNode.property.name === 'mount') { + return { + kind: 'chain', + mountCall: callNode, + }; + } + + return VuePluginCodemod.walkChain(callNode, parents, index + 2); + } + + private static matchVariableAnchor(match: CreateAppMatch): VariableAnchor | null { + const [declaratorNode, declarationNode, containerNode] = match.parents; + + if (!t.isVariableDeclarator(declaratorNode) || declaratorNode.init !== match.call) { + return null; + } + + if (!t.isIdentifier(declaratorNode.id)) { + return null; + } + + const parentBody = VuePluginCodemod.extractStatementBody(containerNode); + + if (parentBody === null) { + return null; + } + + const declarationIndex = parentBody.findIndex(statement => statement === declarationNode); + const binding = declaratorNode.id.name; + const mountStatement = VuePluginCodemod.findMountStatement(parentBody, binding, declarationIndex + 1); + + if (mountStatement === null) { + return null; + } + + return { + kind: 'variable', + binding: binding, + parentBody: parentBody, + declarationStatement: declarationNode, + mountStatement: mountStatement, + }; + } + + private static extractStatementBody(container: t.Node | undefined): t.Statement[] | null { + if ( + container !== undefined + && (t.isProgram(container) || t.isBlockStatement(container) || t.isStaticBlock(container)) + ) { + return container.body; + } + + return null; + } + + private static findMountStatement( + body: t.Statement[], + binding: string, + from: number, + ): t.ExpressionStatement | null { + for (let i = from; i < body.length; i++) { + const statement = body[i]; + + if (!t.isExpressionStatement(statement) || !t.isCallExpression(statement.expression)) { + continue; + } + + const {callee} = statement.expression; + + if (!t.isMemberExpression(callee) || callee.computed) { + continue; + } + + if (!t.isIdentifier(callee.object) || callee.object.name !== binding) { + continue; + } + + if (!t.isIdentifier(callee.property) || callee.property.name !== 'mount') { + continue; + } + + return statement; + } + + return null; + } + + private static isChainCall(node: t.Node): node is ChainCall { + return t.isCallExpression(node) && t.isMemberExpression(node.callee); + } + + private static chainHasFactoryCall(anchor: ChainAnchor, factoryLocal: string): boolean { + let {object} = anchor.mountCall.callee; + + while (VuePluginCodemod.isChainCall(object)) { + if ( + t.isIdentifier(object.callee.property) + && object.callee.property.name === 'use' + && VuePluginCodemod.isFactoryInvocation(object.arguments[0], factoryLocal) + ) { + return true; + } + + object = object.callee.object; + } + + return false; + } + + private static variableBodyHasFactoryCall(anchor: VariableAnchor, factoryLocal: string): boolean { + for (const useCall of VuePluginCodemod.iterateUseCalls(anchor)) { + if (VuePluginCodemod.isFactoryInvocation(useCall.arguments[0], factoryLocal)) { + return true; + } + } + + return false; + } + + private static findUseCallInChain(anchor: ChainAnchor, identifier: string): t.CallExpression | null { + let current: t.Node = anchor.mountCall.callee.object; + + while (VuePluginCodemod.isChainCall(current)) { + if ( + t.isIdentifier(current.callee.property) + && current.callee.property.name === 'use' + && VuePluginCodemod.isFirstArgNamed(current.arguments, identifier) + ) { + return current; + } + + current = current.callee.object; + } + + return null; + } + + private static findUseCallInBody(anchor: VariableAnchor, identifier: string): t.CallExpression | null { + for (const useCall of VuePluginCodemod.iterateUseCalls(anchor)) { + if (VuePluginCodemod.isFirstArgNamed(useCall.arguments, identifier)) { + return useCall; + } + } + + return null; + } + + private static* iterateUseCalls(anchor: VariableAnchor): IterableIterator { + let yielding = false; + + for (const statement of anchor.parentBody) { + if (statement === anchor.mountStatement) { + return; + } + + if (!yielding) { + if (statement === anchor.declarationStatement) { + yielding = true; + } + + continue; + } + + if (!t.isExpressionStatement(statement) || !t.isCallExpression(statement.expression)) { + continue; + } + + const {callee} = statement.expression; + + if (!t.isMemberExpression(callee) || callee.computed) { + continue; + } + + if (!t.isIdentifier(callee.object) || callee.object.name !== anchor.binding) { + continue; + } + + if (!t.isIdentifier(callee.property) || callee.property.name !== 'use') { + continue; + } + + yield statement.expression; + } + } + + private static isFactoryInvocation(node: t.Node | undefined, factoryLocal: string): boolean { + return node !== undefined + && t.isCallExpression(node) + && t.isIdentifier(node.callee) + && node.callee.name === factoryLocal; + } + + private static isFirstArgNamed(args: t.Node[], identifier: string): boolean { + const [first] = args; + + return first !== undefined && t.isIdentifier(first) && first.name === identifier; + } + + private static injectIntoChain(anchor: ChainAnchor, factoryCall: t.CallExpression): void { + const {mountCall} = anchor; + const receiver = mountCall.callee.object; + + mountCall.callee.object = t.callExpression( + t.memberExpression(receiver, t.identifier('use')), + [factoryCall], + ); + } + + private static injectBeforeMount(anchor: VariableAnchor, factoryCall: t.CallExpression): void { + const useStatement = t.expressionStatement( + t.callExpression( + t.memberExpression(t.identifier(anchor.binding), t.identifier('use')), + [factoryCall], + ), + ); + + const mountIndex = anchor.parentBody.findIndex(statement => statement === anchor.mountStatement); + + anchor.parentBody.splice(mountIndex, 0, useStatement); + } +} diff --git a/src/application/project/code/transformation/javascript/vueStoryblokCodemod.ts b/src/application/project/code/transformation/javascript/vueStoryblokCodemod.ts new file mode 100644 index 00000000..ca0b5df0 --- /dev/null +++ b/src/application/project/code/transformation/javascript/vueStoryblokCodemod.ts @@ -0,0 +1,121 @@ +import * as t from '@babel/types'; +import {traverse} from '@babel/core'; +import type {Codemod, CodemodOptions, ResultCode} from '@/application/project/code/transformation/codemod'; +import {addImport} from '@/application/project/code/transformation/javascript/utils/addImport'; +import {getImportLocalName} from '@/application/project/code/transformation/javascript/utils/getImportLocalName'; +import {VuePluginCodemod} from '@/application/project/code/transformation/javascript/vuePluginCodemod'; + +export type VueStoryblokConfiguration = { + plugin: { + module: string, + factory: string, + }, + storyblok: { + module: string, + identifier: string, + }, +}; + +/** + * Migrates an existing Storyblok Vue plugin registration to Croct. + * + * Rewrites the use call so the original options are passed through the Croct + * Storyblok factory, drops the Storyblok identifier from its import, and adds + * the new factory import. If the migration is already in place, the codemod + * returns unmodified. + */ +export class VueStoryblokCodemod implements Codemod { + private readonly configuration: VueStoryblokConfiguration; + + public constructor(configuration: VueStoryblokConfiguration) { + this.configuration = configuration; + } + + public apply(input: t.File): Promise> { + const anchor = VuePluginCodemod.findMountAnchor(input); + + if (anchor === null) { + return Promise.resolve({modified: false, result: input}); + } + + const {plugin, storyblok} = this.configuration; + + const factoryLocal = getImportLocalName(input, { + moduleName: plugin.module, + importName: plugin.factory, + }); + + if (factoryLocal !== null && VuePluginCodemod.hasFactoryCall(anchor, factoryLocal)) { + return Promise.resolve({modified: false, result: input}); + } + + const storyblokLocal = getImportLocalName(input, { + moduleName: storyblok.module, + importName: storyblok.identifier, + }); + + if (storyblokLocal === null) { + return Promise.resolve({modified: false, result: input}); + } + + const useCall = VuePluginCodemod.findUseCall(anchor, storyblokLocal); + + if (useCall === null) { + return Promise.resolve({modified: false, result: input}); + } + + const factoryImport = addImport(input, { + type: 'value', + moduleName: plugin.module, + importName: plugin.factory, + }); + + const options = useCall.arguments.slice(1); + + useCall.arguments = [ + t.callExpression( + t.identifier(factoryImport.localName), + options.filter((node): node is t.Expression => !t.isJSXNamespacedName(node)), + ), + ]; + + VueStoryblokCodemod.dropImportSpecifier(input, storyblok.module, storyblok.identifier); + + return Promise.resolve({modified: true, result: input}); + } + + private static dropImportSpecifier(ast: t.File, moduleName: string, importName: string): void { + traverse(ast, { + ImportDeclaration: path => { + const {node} = path; + + if (node.source.value !== moduleName) { + return path.skip(); + } + + const remaining = node.specifiers.filter(specifier => { + if (!t.isImportSpecifier(specifier)) { + return true; + } + + const {imported} = specifier; + const importedName = t.isIdentifier(imported) ? imported.name : imported.value; + + return importedName !== importName; + }); + + if (remaining.length === 0) { + path.remove(); + + return; + } + + if (remaining.length !== node.specifiers.length) { + node.specifiers = remaining; + } + + return path.skip(); + }, + }); + } +} diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/aliasedDefineNuxtConfig.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/aliasedDefineNuxtConfig.ts new file mode 100644 index 00000000..8fd5bb81 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/aliasedDefineNuxtConfig.ts @@ -0,0 +1,5 @@ +import { defineNuxtConfig as defineConfig } from 'nuxt/config'; + +export default defineConfig({ + modules: ['@nuxtjs/tailwindcss'], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/aliasedIndirectConfig.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/aliasedIndirectConfig.ts new file mode 100644 index 00000000..67d9a8b2 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/aliasedIndirectConfig.ts @@ -0,0 +1,7 @@ +import { defineNuxtConfig as defineConfig } from 'nuxt/config'; + +const config = defineConfig({ + modules: ['@nuxtjs/tailwindcss'], +}); + +export default config; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/alreadyConfigured.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/alreadyConfigured.ts new file mode 100644 index 00000000..d3105a41 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/alreadyConfigured.ts @@ -0,0 +1,3 @@ +export default defineNuxtConfig({ + modules: ['@croct/plug-nuxt'], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/alreadyConfiguredAsTuple.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/alreadyConfiguredAsTuple.ts new file mode 100644 index 00000000..abe5dbd9 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/alreadyConfiguredAsTuple.ts @@ -0,0 +1,5 @@ +export default defineNuxtConfig({ + modules: [ + ['@croct/plug-nuxt', { debug: true }], + ], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectExport.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectExport.ts new file mode 100644 index 00000000..4a89b941 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectExport.ts @@ -0,0 +1,3 @@ +export default { + modules: ['@nuxtjs/tailwindcss'], +}; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectExportNoModules.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectExportNoModules.ts new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectExportNoModules.ts @@ -0,0 +1 @@ +export default {}; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectIndirectVariable.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectIndirectVariable.ts new file mode 100644 index 00000000..1a32d930 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/bareObjectIndirectVariable.ts @@ -0,0 +1,5 @@ +const config = { + modules: ['@nuxtjs/tailwindcss'], +}; + +export default config; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/defineCallWithReference.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/defineCallWithReference.ts new file mode 100644 index 00000000..286231ab --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/defineCallWithReference.ts @@ -0,0 +1,3 @@ +const opts = { modules: [] }; + +export default defineNuxtConfig(opts); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/emptyDefineCall.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/emptyDefineCall.ts new file mode 100644 index 00000000..a1e8f808 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/emptyDefineCall.ts @@ -0,0 +1 @@ +export default defineNuxtConfig(); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/emptyModulesArray.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/emptyModulesArray.ts new file mode 100644 index 00000000..3b54fe11 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/emptyModulesArray.ts @@ -0,0 +1,3 @@ +export default defineNuxtConfig({ + modules: [], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/existingModules.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/existingModules.ts new file mode 100644 index 00000000..f270dee8 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/existingModules.ts @@ -0,0 +1,3 @@ +export default defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss'], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/functionDefaultExport.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/functionDefaultExport.ts new file mode 100644 index 00000000..e3ab1f48 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/functionDefaultExport.ts @@ -0,0 +1,3 @@ +export default function makeConfig() { + return { modules: [] }; +} diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/indirectConfigVariable.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/indirectConfigVariable.ts new file mode 100644 index 00000000..0a9aa4c9 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/indirectConfigVariable.ts @@ -0,0 +1,5 @@ +const config = defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss'], +}); + +export default config; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/indirectConfigWithExtraDeclarator.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/indirectConfigWithExtraDeclarator.ts new file mode 100644 index 00000000..2659987e --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/indirectConfigWithExtraDeclarator.ts @@ -0,0 +1,7 @@ +const unrelated = 1; +const config = defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss'], +}); + +export default config; +export { unrelated }; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/modulesAsSpread.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/modulesAsSpread.ts new file mode 100644 index 00000000..aaec0536 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/modulesAsSpread.ts @@ -0,0 +1,5 @@ +const sharedModules = ['@nuxtjs/tailwindcss']; + +export default defineNuxtConfig({ + modules: [...sharedModules], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/noModulesKey.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/noModulesKey.ts new file mode 100644 index 00000000..68093d8e --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/noModulesKey.ts @@ -0,0 +1 @@ +export default defineNuxtConfig({}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/nonArrayModules.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/nonArrayModules.ts new file mode 100644 index 00000000..2018c59b --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/nonArrayModules.ts @@ -0,0 +1,5 @@ +const modules = ['@nuxtjs/tailwindcss']; + +export default defineNuxtConfig({ + modules: modules, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/nonObjectExport.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/nonObjectExport.ts new file mode 100644 index 00000000..a54f8321 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/nonObjectExport.ts @@ -0,0 +1 @@ +export default makeConfig(); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/sparseModulesArray.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/sparseModulesArray.ts new file mode 100644 index 00000000..40635df2 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/sparseModulesArray.ts @@ -0,0 +1,3 @@ +export default defineNuxtConfig({ + modules: [, '@nuxtjs/tailwindcss'], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/spreadConfigProperties.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/spreadConfigProperties.ts new file mode 100644 index 00000000..69e8bdf8 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/spreadConfigProperties.ts @@ -0,0 +1,5 @@ +const shared = { modules: ['@nuxtjs/tailwindcss'] }; + +export default defineNuxtConfig({ + ...shared, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/stringLiteralModulesKey.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/stringLiteralModulesKey.ts new file mode 100644 index 00000000..e2bba6b3 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/stringLiteralModulesKey.ts @@ -0,0 +1,3 @@ +export default defineNuxtConfig({ + 'modules': ['@nuxtjs/tailwindcss'], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/uninitializedBinding.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/uninitializedBinding.ts new file mode 100644 index 00000000..929f8a20 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/uninitializedBinding.ts @@ -0,0 +1,3 @@ +let config; + +export default config; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/withStoryblokModule.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/withStoryblokModule.ts new file mode 100644 index 00000000..c6ac16d0 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/withStoryblokModule.ts @@ -0,0 +1,5 @@ +export default defineNuxtConfig({ + modules: [ + ['@storyblok/nuxt', { accessToken: 'YOUR_ACCESS_TOKEN' }], + ], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/withTypeAssertionAsConst.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/withTypeAssertionAsConst.ts new file mode 100644 index 00000000..24774ce7 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/withTypeAssertionAsConst.ts @@ -0,0 +1,3 @@ +export default defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss'], +} as const); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config-module/withTypeAssertionSatisfies.ts b/test/application/project/code/transformation/fixtures/nuxt-config-module/withTypeAssertionSatisfies.ts new file mode 100644 index 00000000..69e7e237 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config-module/withTypeAssertionSatisfies.ts @@ -0,0 +1,5 @@ +import type { NuxtConfig } from 'nuxt/schema'; + +export default defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss'], +} satisfies NuxtConfig); diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/aliasedDefineNuxtPlugin.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/aliasedDefineNuxtPlugin.ts new file mode 100644 index 00000000..31fd3be1 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/aliasedDefineNuxtPlugin.ts @@ -0,0 +1,5 @@ +import { defineNuxtPlugin as defineNuxt } from '#app'; + +export default defineNuxt(nuxtApp => { + console.log('custom plugin', nuxtApp); +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/alreadyScaffolded.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/alreadyScaffolded.ts new file mode 100644 index 00000000..2ca84d8e --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/alreadyScaffolded.ts @@ -0,0 +1,7 @@ +import { withCroct } from '@croct/plug-storyblok/nuxt'; +import { useStoryblokApi } from '@storyblok/vue'; +import { defineNuxtPlugin } from '#app'; + +export default defineNuxtPlugin(nuxtApp => { + withCroct(nuxtApp, useStoryblokApi()); +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/empty.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/empty.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/partialScaffold.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/partialScaffold.ts new file mode 100644 index 00000000..df5c8814 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/partialScaffold.ts @@ -0,0 +1,5 @@ +import { defineNuxtPlugin } from '#app'; + +export default defineNuxtPlugin(nuxtApp => { + console.log('custom plugin', nuxtApp); +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/withComments.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/withComments.ts new file mode 100644 index 00000000..09038338 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/withComments.ts @@ -0,0 +1,2 @@ +// Croct + Storyblok bridge plugin. +// Generated by `croct install`. diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/aliasedFactoryImportAndUsed.ts b/test/application/project/code/transformation/fixtures/vue-plugin/aliasedFactoryImportAndUsed.ts new file mode 100644 index 00000000..ab2d0e37 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/aliasedFactoryImportAndUsed.ts @@ -0,0 +1,7 @@ +import { createApp } from 'vue'; +import { createCroct as plug } from '@croct/plug-vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(plug({ appId: 'YOUR_APP_ID' })); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/alreadyRegistered.ts b/test/application/project/code/transformation/fixtures/vue-plugin/alreadyRegistered.ts new file mode 100644 index 00000000..0742d36e --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/alreadyRegistered.ts @@ -0,0 +1,7 @@ +import { createApp } from 'vue'; +import { createCroct } from '@croct/plug-vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(createCroct({ appId: 'YOUR_APP_ID' })); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/argsReference.ts b/test/application/project/code/transformation/fixtures/vue-plugin/argsReference.ts new file mode 100644 index 00000000..b0788dd0 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/argsReference.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/argsTernary.ts b/test/application/project/code/transformation/fixtures/vue-plugin/argsTernary.ts new file mode 100644 index 00000000..b0788dd0 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/argsTernary.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/chainedAlreadyRegistered.ts b/test/application/project/code/transformation/fixtures/vue-plugin/chainedAlreadyRegistered.ts new file mode 100644 index 00000000..659cee7a --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/chainedAlreadyRegistered.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import { createCroct } from '@croct/plug-vue'; +import App from './App.vue'; + +createApp(App).use(createCroct({ appId: 'YOUR_APP_ID' })).mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/chainedMount.ts b/test/application/project/code/transformation/fixtures/vue-plugin/chainedMount.ts new file mode 100644 index 00000000..684d0421 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/chainedMount.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +createApp(App).mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/chainedWithPlugins.ts b/test/application/project/code/transformation/fixtures/vue-plugin/chainedWithPlugins.ts new file mode 100644 index 00000000..64943a35 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/chainedWithPlugins.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue'; +import router from './router'; +import pinia from './pinia'; +import App from './App.vue'; + +createApp(App).use(router).use(pinia).mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/computedMountAccess.ts b/test/application/project/code/transformation/fixtures/vue-plugin/computedMountAccess.ts new file mode 100644 index 00000000..26dd4d5d --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/computedMountAccess.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app['mount']('#app'); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/createAppAsArgument.ts b/test/application/project/code/transformation/fixtures/vue-plugin/createAppAsArgument.ts new file mode 100644 index 00000000..b297931c --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/createAppAsArgument.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +console.log(createApp(App)); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/createAppFromOtherPackage.ts b/test/application/project/code/transformation/fixtures/vue-plugin/createAppFromOtherPackage.ts new file mode 100644 index 00000000..f1666979 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/createAppFromOtherPackage.ts @@ -0,0 +1,3 @@ +import { createApp } from 'other-package'; + +createApp({}).mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/createAppImportedButNotCalled.ts b/test/application/project/code/transformation/fixtures/vue-plugin/createAppImportedButNotCalled.ts new file mode 100644 index 00000000..9593c811 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/createAppImportedButNotCalled.ts @@ -0,0 +1,3 @@ +import { createApp } from 'vue'; + +console.log(createApp); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/destructuredCreateApp.ts b/test/application/project/code/transformation/fixtures/vue-plugin/destructuredCreateApp.ts new file mode 100644 index 00000000..97a2de23 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/destructuredCreateApp.ts @@ -0,0 +1,7 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const { mount } = createApp(App); + +mount('#app'); + diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/exportedCreateApp.ts b/test/application/project/code/transformation/fixtures/vue-plugin/exportedCreateApp.ts new file mode 100644 index 00000000..87c45ebb --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/exportedCreateApp.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +export const app = createApp(App); + +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/importedAliasedButNotUsed.ts b/test/application/project/code/transformation/fixtures/vue-plugin/importedAliasedButNotUsed.ts new file mode 100644 index 00000000..03a49ae1 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/importedAliasedButNotUsed.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue'; +import { createCroct as plug } from '@croct/plug-vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/importedButNotUsed.ts b/test/application/project/code/transformation/fixtures/vue-plugin/importedButNotUsed.ts new file mode 100644 index 00000000..e39bcc8f --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/importedButNotUsed.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue'; +import { createCroct } from '@croct/plug-vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/incompleteChain.ts b/test/application/project/code/transformation/fixtures/vue-plugin/incompleteChain.ts new file mode 100644 index 00000000..3ffe1e78 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/incompleteChain.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const mountFn = createApp(App).mount; +mountFn('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/intermediateNonUseStatement.ts b/test/application/project/code/transformation/fixtures/vue-plugin/intermediateNonUseStatement.ts new file mode 100644 index 00000000..3aa02318 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/intermediateNonUseStatement.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +const flag = true; +app.use(router); +console.log(flag); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/intermediateUnrelatedCalls.ts b/test/application/project/code/transformation/fixtures/vue-plugin/intermediateUnrelatedCalls.ts new file mode 100644 index 00000000..82d75b8f --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/intermediateUnrelatedCalls.ts @@ -0,0 +1,11 @@ +import { createApp } from 'vue'; +import { createCroct } from '@croct/plug-vue'; +import App from './App.vue'; + +const app = createApp(App); +app['use'](router); +app.config.globalProperties.foo = 'bar'; +console.log('hi'); +app.mixin({}); +app.mount('#app'); + diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/mountWithDocumentBody.ts b/test/application/project/code/transformation/fixtures/vue-plugin/mountWithDocumentBody.ts new file mode 100644 index 00000000..f32505d0 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/mountWithDocumentBody.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount(document.body); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/multipleCreateAppCallsChainedFirst.ts b/test/application/project/code/transformation/fixtures/vue-plugin/multipleCreateAppCallsChainedFirst.ts new file mode 100644 index 00000000..17c46be8 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/multipleCreateAppCallsChainedFirst.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import Other from './Other.vue'; + +createApp(App).mount('#app'); + +const second = createApp(Other); +second.mount('#second'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/multipleCreateAppCallsVariableFirst.ts b/test/application/project/code/transformation/fixtures/vue-plugin/multipleCreateAppCallsVariableFirst.ts new file mode 100644 index 00000000..eab42988 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/multipleCreateAppCallsVariableFirst.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import Other from './Other.vue'; + +const app = createApp(App); +app.mount('#app'); + +createApp(Other).mount('#second'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/noArgs.ts b/test/application/project/code/transformation/fixtures/vue-plugin/noArgs.ts new file mode 100644 index 00000000..b0788dd0 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/noArgs.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/noCreateAppCall.ts b/test/application/project/code/transformation/fixtures/vue-plugin/noCreateAppCall.ts new file mode 100644 index 00000000..99cf8c91 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/noCreateAppCall.ts @@ -0,0 +1,3 @@ +import { something } from 'somewhere'; + +something({ foo: 'bar' }); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/noMountCall.ts b/test/application/project/code/transformation/fixtures/vue-plugin/noMountCall.ts new file mode 100644 index 00000000..728cc113 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/noMountCall.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(router); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/typescriptInput.ts b/test/application/project/code/transformation/fixtures/vue-plugin/typescriptInput.ts new file mode 100644 index 00000000..153b0292 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/typescriptInput.ts @@ -0,0 +1,5 @@ +import { createApp, App as VueApp } from 'vue'; +import App from './App.vue'; + +const app: VueApp = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/variableForm.ts b/test/application/project/code/transformation/fixtures/vue-plugin/variableForm.ts new file mode 100644 index 00000000..b0788dd0 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/variableForm.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-plugin/variableWithPlugins.ts b/test/application/project/code/transformation/fixtures/vue-plugin/variableWithPlugins.ts new file mode 100644 index 00000000..1db86670 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-plugin/variableWithPlugins.ts @@ -0,0 +1,9 @@ +import { createApp } from 'vue'; +import router from './router'; +import pinia from './pinia'; +import App from './App.vue'; + +const app = createApp(App); +app.use(router); +app.use(pinia); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokAliasedImport.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokAliasedImport.ts new file mode 100644 index 00000000..0f88dd91 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokAliasedImport.ts @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import { StoryblokVue as SbVue, apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(SbVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +}); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokAlreadyMigrated.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokAlreadyMigrated.ts new file mode 100644 index 00000000..2363ebc9 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokAlreadyMigrated.ts @@ -0,0 +1,11 @@ +import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import { withCroct } from '@croct/plug-storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokBasicMigration.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokBasicMigration.ts new file mode 100644 index 00000000..9c50f603 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokBasicMigration.ts @@ -0,0 +1,11 @@ +import { createApp } from 'vue'; +import { StoryblokVue, apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], + components: {}, +}); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokChainedAppUse.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokChainedAppUse.ts new file mode 100644 index 00000000..f84a9d54 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokChainedAppUse.ts @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import { StoryblokVue, apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; + +createApp(App) + .use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], + }) + .mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokImportedButNoUseCall.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokImportedButNoUseCall.ts new file mode 100644 index 00000000..135e8bf3 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokImportedButNoUseCall.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue'; +import { StoryblokVue } from '@storyblok/vue'; +import App from './App.vue'; + +console.log(StoryblokVue); + +const app = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokMultipleAppUseCalls.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokMultipleAppUseCalls.ts new file mode 100644 index 00000000..199b0f40 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokMultipleAppUseCalls.ts @@ -0,0 +1,14 @@ +import { createApp } from 'vue'; +import { StoryblokVue, apiPlugin } from '@storyblok/vue'; +import router from './router'; +import pinia from './pinia'; +import App from './App.vue'; + +const app = createApp(App); +app.use(router); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +}); +app.use(pinia); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokNoCreateApp.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokNoCreateApp.ts new file mode 100644 index 00000000..6bb3fee2 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokNoCreateApp.ts @@ -0,0 +1,6 @@ +import { StoryblokVue } from '@storyblok/vue'; + +const app = { use: () => app, mount: () => {} }; + +app.use(StoryblokVue, { accessToken: 'YOUR_ACCESS_TOKEN' }); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokNoImport.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokNoImport.ts new file mode 100644 index 00000000..fbe05305 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokNoImport.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokOnlySpecifier.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokOnlySpecifier.ts new file mode 100644 index 00000000..409e696c --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokOnlySpecifier.ts @@ -0,0 +1,9 @@ +import { createApp } from 'vue'; +import { StoryblokVue } from '@storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', +}); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokOptionsArePreserved.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokOptionsArePreserved.ts new file mode 100644 index 00000000..a11a0602 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokOptionsArePreserved.ts @@ -0,0 +1,15 @@ +import { createApp } from 'vue'; +import { StoryblokVue, apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; +import MyComponent from './MyComponent.vue'; + +const sharedOptions = { foo: 'bar' }; + +const app = createApp(App); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], + components: { MyComponent }, + ...sharedOptions, +}); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokStringSpecifier.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokStringSpecifier.ts new file mode 100644 index 00000000..b02a2e22 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokStringSpecifier.ts @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import { 'apiPlugin' as apiPlugin, StoryblokVue } from '@storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +}); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithCroctAliasedButNotUsed.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithCroctAliasedButNotUsed.ts new file mode 100644 index 00000000..aaa7526b --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithCroctAliasedButNotUsed.ts @@ -0,0 +1,11 @@ +import { createApp } from 'vue'; +import { StoryblokVue, apiPlugin } from '@storyblok/vue'; +import { withCroct as croctSb } from '@croct/plug-storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +}); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithCroctImportedButNotUsed.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithCroctImportedButNotUsed.ts new file mode 100644 index 00000000..edd5f8d7 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithCroctImportedButNotUsed.ts @@ -0,0 +1,11 @@ +import { createApp } from 'vue'; +import { StoryblokVue, apiPlugin } from '@storyblok/vue'; +import { withCroct } from '@croct/plug-storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +}); +app.mount('#app'); diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithDefaultSpecifier.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithDefaultSpecifier.ts new file mode 100644 index 00000000..d61886de --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithDefaultSpecifier.ts @@ -0,0 +1,12 @@ +import { createApp } from 'vue'; +import StoryblokDefault, { StoryblokVue, apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +}); +app.mount('#app'); + +export { StoryblokDefault }; diff --git a/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithOtherSpecifiers.ts b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithOtherSpecifiers.ts new file mode 100644 index 00000000..f243ce47 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/vue-storyblok/storyblokWithOtherSpecifiers.ts @@ -0,0 +1,12 @@ +import { createApp } from 'vue'; +import { StoryblokVue, apiPlugin, useStoryblok } from '@storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(StoryblokVue, { + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +}); +app.mount('#app'); + +export { useStoryblok }; diff --git a/test/application/project/code/transformation/javascript/__snapshots__/nuxtConfigModuleCodemod.test.ts.snap b/test/application/project/code/transformation/javascript/__snapshots__/nuxtConfigModuleCodemod.test.ts.snap new file mode 100644 index 00000000..abf2d589 --- /dev/null +++ b/test/application/project/code/transformation/javascript/__snapshots__/nuxtConfigModuleCodemod.test.ts.snap @@ -0,0 +1,200 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NuxtConfigModuleCodemod should correctly transform aliasedDefineNuxtConfig.ts: aliasedDefineNuxtConfig.ts 1`] = ` +"import { defineNuxtConfig as defineConfig } from 'nuxt/config'; + +export default defineConfig({ + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform aliasedIndirectConfig.ts: aliasedIndirectConfig.ts 1`] = ` +"import { defineNuxtConfig as defineConfig } from 'nuxt/config'; + +const config = defineConfig({ + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}); + +export default config; +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform alreadyConfigured.ts: alreadyConfigured.ts 1`] = ` +"export default defineNuxtConfig({ + modules: ['@croct/plug-nuxt'], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform alreadyConfiguredAsTuple.ts: alreadyConfiguredAsTuple.ts 1`] = ` +"export default defineNuxtConfig({ + modules: [ + ['@croct/plug-nuxt', { debug: true }], + ], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform bareObjectExport.ts: bareObjectExport.ts 1`] = ` +"export default { + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}; +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform bareObjectExportNoModules.ts: bareObjectExportNoModules.ts 1`] = ` +"export default { + modules: ["@croct/plug-nuxt"] +}; +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform bareObjectIndirectVariable.ts: bareObjectIndirectVariable.ts 1`] = ` +"const config = { + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}; + +export default config; +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform defineCallWithReference.ts: defineCallWithReference.ts 1`] = ` +"const opts = { modules: [] }; + +export default defineNuxtConfig(opts); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform emptyDefineCall.ts: emptyDefineCall.ts 1`] = ` +"export default defineNuxtConfig(); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform emptyModulesArray.ts: emptyModulesArray.ts 1`] = ` +"export default defineNuxtConfig({ + modules: ["@croct/plug-nuxt"], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform existingModules.ts: existingModules.ts 1`] = ` +"export default defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform functionDefaultExport.ts: functionDefaultExport.ts 1`] = ` +"export default function makeConfig() { + return { modules: [] }; +} +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform indirectConfigVariable.ts: indirectConfigVariable.ts 1`] = ` +"const config = defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}); + +export default config; +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform indirectConfigWithExtraDeclarator.ts: indirectConfigWithExtraDeclarator.ts 1`] = ` +"const unrelated = 1; +const config = defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}); + +export default config; +export { unrelated }; +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform modulesAsSpread.ts: modulesAsSpread.ts 1`] = ` +"const sharedModules = ['@nuxtjs/tailwindcss']; + +export default defineNuxtConfig({ + modules: [...sharedModules, "@croct/plug-nuxt"], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform noModulesKey.ts: noModulesKey.ts 1`] = ` +"export default defineNuxtConfig({ + modules: ["@croct/plug-nuxt"] +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform nonArrayModules.ts: nonArrayModules.ts 1`] = ` +"const modules = ['@nuxtjs/tailwindcss']; + +export default defineNuxtConfig({ + modules: modules, +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform nonObjectExport.ts: nonObjectExport.ts 1`] = ` +"export default makeConfig(); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform sparseModulesArray.ts: sparseModulesArray.ts 1`] = ` +"export default defineNuxtConfig({ + modules: [, '@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform spreadConfigProperties.ts: spreadConfigProperties.ts 1`] = ` +"const shared = { modules: ['@nuxtjs/tailwindcss'] }; + +export default defineNuxtConfig({ + ...shared, + modules: ["@croct/plug-nuxt"] +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform stringLiteralModulesKey.ts: stringLiteralModulesKey.ts 1`] = ` +"export default defineNuxtConfig({ + 'modules': ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform uninitializedBinding.ts: uninitializedBinding.ts 1`] = ` +"let config; + +export default config; +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform withStoryblokModule.ts: withStoryblokModule.ts 1`] = ` +"export default defineNuxtConfig({ + modules: [ + ['@storyblok/nuxt', { accessToken: 'YOUR_ACCESS_TOKEN' }], + "@croct/plug-nuxt" + ], +}); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform withTypeAssertionAsConst.ts: withTypeAssertionAsConst.ts 1`] = ` +"export default defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +} as const); +" +`; + +exports[`NuxtConfigModuleCodemod should correctly transform withTypeAssertionSatisfies.ts: withTypeAssertionSatisfies.ts 1`] = ` +"import type { NuxtConfig } from 'nuxt/schema'; + +export default defineNuxtConfig({ + modules: ['@nuxtjs/tailwindcss', "@croct/plug-nuxt"], +} satisfies NuxtConfig); +" +`; diff --git a/test/application/project/code/transformation/javascript/__snapshots__/nuxtStoryblokPluginCodemod.test.ts.snap b/test/application/project/code/transformation/javascript/__snapshots__/nuxtStoryblokPluginCodemod.test.ts.snap new file mode 100644 index 00000000..ed4ae8ab --- /dev/null +++ b/test/application/project/code/transformation/javascript/__snapshots__/nuxtStoryblokPluginCodemod.test.ts.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NuxtStoryblokPluginCodemod should correctly transform aliasedDefineNuxtPlugin.ts: aliasedDefineNuxtPlugin.ts 1`] = ` +"import { defineNuxtPlugin as defineNuxt } from '#app'; + +export default defineNuxt(nuxtApp => { + console.log('custom plugin', nuxtApp); +}); +" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform alreadyScaffolded.ts: alreadyScaffolded.ts 1`] = ` +"import { withCroct } from '@croct/plug-storyblok/nuxt'; +import { useStoryblokApi } from '@storyblok/vue'; +import { defineNuxtPlugin } from '#app'; + +export default defineNuxtPlugin(nuxtApp => { + withCroct(nuxtApp, useStoryblokApi()); +}); +" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform empty.ts: empty.ts 1`] = ` +"import { defineNuxtPlugin } from "#app"; +import { useStoryblokApi } from "@storyblok/vue"; +import { withCroct } from "@croct/plug-storyblok/nuxt"; + +export default defineNuxtPlugin(nuxtApp => { + withCroct(nuxtApp, useStoryblokApi()); +});" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform partialScaffold.ts: partialScaffold.ts 1`] = ` +"import { defineNuxtPlugin } from '#app'; + +export default defineNuxtPlugin(nuxtApp => { + console.log('custom plugin', nuxtApp); +}); +" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform withComments.ts: withComments.ts 1`] = ` +"import { defineNuxtPlugin } from "#app"; +import { useStoryblokApi } from "@storyblok/vue"; +import { withCroct } from "@croct/plug-storyblok/nuxt"; + +export default defineNuxtPlugin(nuxtApp => { + withCroct(nuxtApp, useStoryblokApi()); +}); +" +`; diff --git a/test/application/project/code/transformation/javascript/__snapshots__/vuePluginCodemod.test.ts.snap b/test/application/project/code/transformation/javascript/__snapshots__/vuePluginCodemod.test.ts.snap new file mode 100644 index 00000000..dc69f8ca --- /dev/null +++ b/test/application/project/code/transformation/javascript/__snapshots__/vuePluginCodemod.test.ts.snap @@ -0,0 +1,332 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VuePluginCodemod apply should correctly transform aliasedFactoryImportAndUsed.ts: aliasedFactoryImportAndUsed.ts 1`] = ` +"import { createApp } from 'vue'; +import { createCroct as plug } from '@croct/plug-vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(plug({ appId: 'YOUR_APP_ID' })); +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform alreadyRegistered.ts: alreadyRegistered.ts 1`] = ` +"import { createApp } from 'vue'; +import { createCroct } from '@croct/plug-vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(createCroct({ appId: 'YOUR_APP_ID' })); +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform argsReference.ts: argsReference.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(createCroct({ + appId: import.meta.env.VITE_CROCT_APP_ID +})); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform argsTernary.ts: argsTernary.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(createCroct({ + appId: import.meta.env.MODE === "production" ? import.meta.env.VITE_CROCT_APP_ID_PROD : import.meta.env.VITE_CROCT_APP_ID_DEV +})); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform chainedAlreadyRegistered.ts: chainedAlreadyRegistered.ts 1`] = ` +"import { createApp } from 'vue'; +import { createCroct } from '@croct/plug-vue'; +import App from './App.vue'; + +createApp(App).use(createCroct({ appId: 'YOUR_APP_ID' })).mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform chainedMount.ts: chainedMount.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; + +createApp(App).use(createCroct({ + appId: "YOUR_APP_ID" +})).mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform chainedWithPlugins.ts: chainedWithPlugins.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import router from './router'; +import pinia from './pinia'; +import App from './App.vue'; + +createApp(App).use(router).use(pinia).use(createCroct({ + appId: "YOUR_APP_ID" +})).mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform computedMountAccess.ts: computedMountAccess.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +const app = createApp(App); +app['mount']('#app'); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform createAppAsArgument.ts: createAppAsArgument.ts 1`] = ` +"import { createApp } from 'vue'; +import App from './App.vue'; + +console.log(createApp(App)); +" +`; + +exports[`VuePluginCodemod apply should correctly transform createAppFromOtherPackage.ts: createAppFromOtherPackage.ts 1`] = ` +"import { createApp } from 'other-package'; + +createApp({}).mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform createAppImportedButNotCalled.ts: createAppImportedButNotCalled.ts 1`] = ` +"import { createApp } from 'vue'; + +console.log(createApp); +" +`; + +exports[`VuePluginCodemod apply should correctly transform destructuredCreateApp.ts: destructuredCreateApp.ts 1`] = ` +"import { createApp } from 'vue'; +import App from './App.vue'; + +const { mount } = createApp(App); + +mount('#app'); + +" +`; + +exports[`VuePluginCodemod apply should correctly transform exportedCreateApp.ts: exportedCreateApp.ts 1`] = ` +"import { createApp } from 'vue'; +import App from './App.vue'; + +export const app = createApp(App); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform importedAliasedButNotUsed.ts: importedAliasedButNotUsed.ts 1`] = ` +"import { createApp } from 'vue'; +import { createCroct as plug } from '@croct/plug-vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(plug({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform importedButNotUsed.ts: importedButNotUsed.ts 1`] = ` +"import { createApp } from 'vue'; +import { createCroct } from '@croct/plug-vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform incompleteChain.ts: incompleteChain.ts 1`] = ` +"import { createApp } from 'vue'; +import App from './App.vue'; + +const mountFn = createApp(App).mount; +mountFn('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform intermediateNonUseStatement.ts: intermediateNonUseStatement.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +const app = createApp(App); +const flag = true; +app.use(router); +console.log(flag); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform intermediateUnrelatedCalls.ts: intermediateUnrelatedCalls.ts 1`] = ` +"import { createApp } from 'vue'; +import { createCroct } from '@croct/plug-vue'; +import App from './App.vue'; +const app = createApp(App); +app['use'](router); +app.config.globalProperties.foo = 'bar'; +console.log('hi'); +app.mixin({}); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); + +" +`; + +exports[`VuePluginCodemod apply should correctly transform mountWithDocumentBody.ts: mountWithDocumentBody.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount(document.body); +" +`; + +exports[`VuePluginCodemod apply should correctly transform multipleCreateAppCallsChainedFirst.ts: multipleCreateAppCallsChainedFirst.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +import Other from './Other.vue'; + +createApp(App).use(createCroct({ + appId: "YOUR_APP_ID" +})).mount('#app'); + +const second = createApp(Other); +second.mount('#second'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform multipleCreateAppCallsVariableFirst.ts: multipleCreateAppCallsVariableFirst.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +import Other from './Other.vue'; +const app = createApp(App); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); +createApp(Other).mount('#second'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform noArgs.ts: noArgs.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +const app = createApp(App); +app.use(createCroct()); +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform noCreateAppCall.ts: noCreateAppCall.ts 1`] = ` +"import { something } from 'somewhere'; + +something({ foo: 'bar' }); +" +`; + +exports[`VuePluginCodemod apply should correctly transform noMountCall.ts: noMountCall.ts 1`] = ` +"import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(router); +" +`; + +exports[`VuePluginCodemod apply should correctly transform typescriptInput.ts: typescriptInput.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp, App as VueApp } from 'vue'; +import App from './App.vue'; +const app: VueApp = createApp(App); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform variableForm.ts: variableForm.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); +" +`; + +exports[`VuePluginCodemod apply should correctly transform variableWithPlugins.ts: variableWithPlugins.ts 1`] = ` +"import { createCroct } from "@croct/plug-vue"; +import { createApp } from 'vue'; +import router from './router'; +import pinia from './pinia'; +import App from './App.vue'; +const app = createApp(App); +app.use(router); +app.use(pinia); + +app.use(createCroct({ + appId: "YOUR_APP_ID" +})); + +app.mount('#app'); +" +`; diff --git a/test/application/project/code/transformation/javascript/__snapshots__/vueStoryblokCodemod.test.ts.snap b/test/application/project/code/transformation/javascript/__snapshots__/vueStoryblokCodemod.test.ts.snap new file mode 100644 index 00000000..13a1d933 --- /dev/null +++ b/test/application/project/code/transformation/javascript/__snapshots__/vueStoryblokCodemod.test.ts.snap @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VueStoryblokCodemod should correctly transform storyblokAliasedImport.ts: storyblokAliasedImport.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); + +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokAlreadyMigrated.ts: storyblokAlreadyMigrated.ts 1`] = ` +"import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import { withCroct } from '@croct/plug-storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokBasicMigration.ts: storyblokBasicMigration.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], + components: {}, +})); + +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokChainedAppUse.ts: storyblokChainedAppUse.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; + +createApp(App) + .use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], + })) + .mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokImportedButNoUseCall.ts: storyblokImportedButNoUseCall.ts 1`] = ` +"import { createApp } from 'vue'; +import { StoryblokVue } from '@storyblok/vue'; +import App from './App.vue'; + +console.log(StoryblokVue); + +const app = createApp(App); +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokMultipleAppUseCalls.ts: storyblokMultipleAppUseCalls.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import router from './router'; +import pinia from './pinia'; +import App from './App.vue'; +const app = createApp(App); +app.use(router); + +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); + +app.use(pinia); +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokNoCreateApp.ts: storyblokNoCreateApp.ts 1`] = ` +"import { StoryblokVue } from '@storyblok/vue'; + +const app = { use: () => app, mount: () => {} }; + +app.use(StoryblokVue, { accessToken: 'YOUR_ACCESS_TOKEN' }); +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokNoImport.ts: storyblokNoImport.ts 1`] = ` +"import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokOnlySpecifier.ts: storyblokOnlySpecifier.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', +})); +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokOptionsArePreserved.ts: storyblokOptionsArePreserved.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; +import MyComponent from './MyComponent.vue'; +const sharedOptions = { foo: 'bar' }; +const app = createApp(App); + +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], + components: { MyComponent }, + ...sharedOptions, +})); + +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokStringSpecifier.ts: storyblokStringSpecifier.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import { 'apiPlugin' as apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); + +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokWithCroctAliasedButNotUsed.ts: storyblokWithCroctAliasedButNotUsed.ts 1`] = ` +"import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import { withCroct as croctSb } from '@croct/plug-storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(croctSb({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokWithCroctImportedButNotUsed.ts: storyblokWithCroctImportedButNotUsed.ts 1`] = ` +"import { createApp } from 'vue'; +import { apiPlugin } from '@storyblok/vue'; +import { withCroct } from '@croct/plug-storyblok/vue'; +import App from './App.vue'; + +const app = createApp(App); +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); +app.mount('#app'); +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokWithDefaultSpecifier.ts: storyblokWithDefaultSpecifier.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import StoryblokDefault, { apiPlugin } from '@storyblok/vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); + +app.mount('#app'); +export { StoryblokDefault }; +" +`; + +exports[`VueStoryblokCodemod should correctly transform storyblokWithOtherSpecifiers.ts: storyblokWithOtherSpecifiers.ts 1`] = ` +"import { withCroct } from "@croct/plug-storyblok/vue"; +import { createApp } from 'vue'; +import { apiPlugin, useStoryblok } from '@storyblok/vue'; +import App from './App.vue'; +const app = createApp(App); + +app.use(withCroct({ + accessToken: 'YOUR_ACCESS_TOKEN', + use: [apiPlugin], +})); + +app.mount('#app'); +export { useStoryblok }; +" +`; diff --git a/test/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.test.ts b/test/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.test.ts new file mode 100644 index 00000000..aed76599 --- /dev/null +++ b/test/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.test.ts @@ -0,0 +1,30 @@ +import {resolve} from 'path'; +import type { + NuxtConfigModuleConfiguration, +} from '@/application/project/code/transformation/javascript/nuxtConfigModuleCodemod'; +import {NuxtConfigModuleCodemod} from '@/application/project/code/transformation/javascript/nuxtConfigModuleCodemod'; +import {JavaScriptCodemod} from '@/application/project/code/transformation/javascript/javaScriptCodemod'; +import {loadFixtures} from '../fixtures'; + +describe('NuxtConfigModuleCodemod', () => { + const defaultOptions: NuxtConfigModuleConfiguration = { + moduleName: '@croct/plug-nuxt', + }; + + const scenarios = loadFixtures( + resolve(__dirname, '../fixtures/nuxt-config-module'), + defaultOptions, + {}, + ); + + it.each(scenarios)('should correctly transform $name', async ({name, fixture, options}) => { + const transformer = new JavaScriptCodemod({ + languages: ['typescript', 'jsx'], + codemod: new NuxtConfigModuleCodemod(options), + }); + + const output = await transformer.apply(fixture); + + expect(output.result).toMatchSnapshot(name); + }); +}); diff --git a/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts b/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts new file mode 100644 index 00000000..bdd6b8f5 --- /dev/null +++ b/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts @@ -0,0 +1,37 @@ +import {resolve} from 'path'; +import type { + NuxtStoryblokPluginConfiguration, +} from '@/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod'; +import { + NuxtStoryblokPluginCodemod, +} from '@/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod'; +import {JavaScriptCodemod} from '@/application/project/code/transformation/javascript/javaScriptCodemod'; +import {loadFixtures} from '../fixtures'; + +describe('NuxtStoryblokPluginCodemod', () => { + const defaultOptions: NuxtStoryblokPluginConfiguration = { + plugin: { + module: '@croct/plug-storyblok/nuxt', + factory: 'withCroct', + }, + storyblokVueModule: '@storyblok/vue', + nuxtAppModule: '#app', + }; + + const scenarios = loadFixtures( + resolve(__dirname, '../fixtures/nuxt-storyblok-plugin'), + defaultOptions, + {}, + ); + + it.each(scenarios)('should correctly transform $name', async ({name, fixture, options}) => { + const transformer = new JavaScriptCodemod({ + languages: ['typescript', 'jsx'], + codemod: new NuxtStoryblokPluginCodemod(options), + }); + + const output = await transformer.apply(fixture); + + expect(output.result).toMatchSnapshot(name); + }); +}); diff --git a/test/application/project/code/transformation/javascript/utils/createObjectProps.test.ts b/test/application/project/code/transformation/javascript/utils/createObjectProps.test.ts new file mode 100644 index 00000000..439931ac --- /dev/null +++ b/test/application/project/code/transformation/javascript/utils/createObjectProps.test.ts @@ -0,0 +1,180 @@ +import * as t from '@babel/types'; +import {generate} from '@babel/generator'; +import type {AttributeType} from '@/application/project/code/transformation/javascript/utils/createObjectProps'; +import { + buildPropertyExpression, + createPropsObject, +} from '@/application/project/code/transformation/javascript/utils/createObjectProps'; + +describe('createObjectProps', () => { + function print(node: t.Node): string { + return generate(node, {concise: true}).code; + } + + describe('buildPropertyExpression', () => { + it('should build a single-segment identifier reference', () => { + const expression = buildPropertyExpression({ + type: 'reference', + path: ['foo'], + }); + + expect(t.isIdentifier(expression)).toBe(true); + expect(print(expression)).toBe('foo'); + }); + + it('should build a two-segment member reference', () => { + const expression = buildPropertyExpression({ + type: 'reference', + path: ['foo', 'bar'], + }); + + expect(print(expression)).toBe('foo.bar'); + }); + + it('should build a deep member reference', () => { + const expression = buildPropertyExpression({ + type: 'reference', + path: ['process', 'env', 'NODE_ENV'], + }); + + expect(print(expression)).toBe('process.env.NODE_ENV'); + }); + + it('should build a string literal', () => { + const expression = buildPropertyExpression({ + type: 'literal', + value: 'hello', + }); + + expect(print(expression)).toBe('"hello"'); + }); + + it('should build a number literal', () => { + const expression = buildPropertyExpression({ + type: 'literal', + value: 42, + }); + + expect(print(expression)).toBe('42'); + }); + + it('should build a boolean literal', () => { + const expression = buildPropertyExpression({ + type: 'literal', + value: true, + }); + + expect(print(expression)).toBe('true'); + }); + + it('should build a null literal', () => { + const expression = buildPropertyExpression({ + type: 'literal', + value: null, + }); + + expect(print(expression)).toBe('null'); + }); + + it('should build a comparison expression', () => { + const expression = buildPropertyExpression({ + type: 'comparison', + operator: '===', + left: {type: 'reference', path: ['process', 'env', 'NODE_ENV']}, + right: {type: 'literal', value: 'production'}, + }); + + expect(print(expression)).toBe('process.env.NODE_ENV === "production"'); + }); + + it('should build a comparison expression with other operators', () => { + const operators: Array' | '>=' | '<' | '<='> = [ + '===', + '!==', + '>', + '>=', + '<', + '<=', + ]; + + for (const operator of operators) { + const expression = buildPropertyExpression({ + type: 'comparison', + operator: operator, + left: {type: 'literal', value: 1}, + right: {type: 'literal', value: 2}, + }); + + expect(print(expression)).toBe(`1 ${operator} 2`); + } + }); + + it('should build a ternary expression', () => { + const expression = buildPropertyExpression({ + type: 'ternary', + condition: { + operator: '===', + left: {type: 'reference', path: ['process', 'env', 'NODE_ENV']}, + right: {type: 'literal', value: 'production'}, + }, + consequent: {type: 'literal', value: 'prod'}, + alternate: {type: 'literal', value: 'dev'}, + }); + + expect(print(expression)).toBe('process.env.NODE_ENV === "production" ? "prod" : "dev"'); + }); + }); + + describe('createPropsObject', () => { + it('should build an empty object expression from no attributes', () => { + const expression = createPropsObject({}); + + expect(t.isObjectExpression(expression)).toBe(true); + expect(expression.properties).toHaveLength(0); + }); + + it('should build an object expression with a single property', () => { + const expression = createPropsObject({ + appId: {type: 'literal', value: 'abc'}, + }); + + expect(print(expression)).toBe('{ appId: "abc" }'); + }); + + it('should build an object expression with multiple properties', () => { + const expression = createPropsObject({ + appId: {type: 'literal', value: 'abc'}, + debug: {type: 'literal', value: true}, + count: {type: 'literal', value: 7}, + }); + + expect(print(expression)).toBe('{ appId: "abc", debug: true, count: 7 }'); + }); + + it('should build an object expression mixing all attribute types', () => { + const expression = createPropsObject({ + literal: {type: 'literal', value: 'l'}, + reference: {type: 'reference', path: ['process', 'env', 'X']}, + ternary: { + type: 'ternary', + condition: { + operator: '===', + left: {type: 'reference', path: ['mode']}, + right: {type: 'literal', value: 'prod'}, + }, + consequent: {type: 'literal', value: 'a'}, + alternate: {type: 'literal', value: 'b'}, + }, + comparison: { + type: 'comparison', + operator: '>', + left: {type: 'literal', value: 1}, + right: {type: 'literal', value: 0}, + }, + }); + + expect(print(expression)) + .toBe('{ literal: "l", reference: process.env.X, ternary: mode === "prod" ? "a" : "b", comparison: 1 > 0 }'); + }); + }); +}); diff --git a/test/application/project/code/transformation/javascript/vuePluginCodemod.test.ts b/test/application/project/code/transformation/javascript/vuePluginCodemod.test.ts new file mode 100644 index 00000000..804f4da9 --- /dev/null +++ b/test/application/project/code/transformation/javascript/vuePluginCodemod.test.ts @@ -0,0 +1,213 @@ +import {resolve} from 'path'; +import {readFileSync} from 'fs'; +import type * as t from '@babel/types'; +import type {VuePluginConfiguration} from '@/application/project/code/transformation/javascript/vuePluginCodemod'; +import {VuePluginCodemod} from '@/application/project/code/transformation/javascript/vuePluginCodemod'; +import {JavaScriptCodemod} from '@/application/project/code/transformation/javascript/javaScriptCodemod'; +import {parse} from '@/application/project/code/transformation/javascript/utils/parse'; +import {loadFixtures} from '../fixtures'; + +describe('VuePluginCodemod', () => { + const fixturesPath = resolve(__dirname, '../fixtures/vue-plugin'); + + const defaultOptions: VuePluginConfiguration = { + plugin: { + module: '@croct/plug-vue', + factory: 'createCroct', + }, + args: { + appId: {type: 'literal', value: 'YOUR_APP_ID'}, + }, + }; + + function loadFixture(name: string): t.File { + return parse(readFileSync(resolve(fixturesPath, name), 'utf-8'), ['typescript', 'jsx']); + } + + it.each( + loadFixtures( + fixturesPath, + defaultOptions, + { + 'argsReference.ts': { + ...defaultOptions, + args: { + appId: { + type: 'reference', + path: ['import', 'meta', 'env', 'VITE_CROCT_APP_ID'], + }, + }, + }, + 'noArgs.ts': { + plugin: defaultOptions.plugin, + args: undefined, + }, + 'argsTernary.ts': { + ...defaultOptions, + args: { + appId: { + type: 'ternary', + condition: { + left: { + type: 'reference', + path: ['import', 'meta', 'env', 'MODE'], + }, + operator: '===', + right: {type: 'literal', value: 'production'}, + }, + consequent: { + type: 'reference', + path: ['import', 'meta', 'env', 'VITE_CROCT_APP_ID_PROD'], + }, + alternate: { + type: 'reference', + path: ['import', 'meta', 'env', 'VITE_CROCT_APP_ID_DEV'], + }, + }, + }, + }, + }, + ), + )('apply should correctly transform $name', async ({name, fixture, options}) => { + const transformer = new JavaScriptCodemod({ + languages: ['typescript', 'jsx'], + codemod: new VuePluginCodemod(options), + }); + + const output = await transformer.apply(fixture); + + expect(output.result).toMatchSnapshot(name); + }); + + it.each<{name: string, fixture: string, expected: object | null}>([ + { + name: 'a chained mount call', + fixture: 'chainedMount.ts', + expected: expect.objectContaining({kind: 'chain'}), + }, + { + name: 'a chained mount call with intermediate plugins', + fixture: 'chainedWithPlugins.ts', + expected: expect.objectContaining({kind: 'chain'}), + }, + { + name: 'a variable-form mount call', + fixture: 'variableForm.ts', + expected: expect.objectContaining({kind: 'variable', binding: 'app'}), + }, + { + name: 'a file without createApp import', + fixture: 'noCreateAppCall.ts', + expected: null, + }, + { + name: 'a file where createApp is imported but never called', + fixture: 'createAppImportedButNotCalled.ts', + expected: null, + }, + { + name: 'a file with createApp call but no .mount', + fixture: 'noMountCall.ts', + expected: null, + }, + { + name: 'multiple createApp calls (only the first is anchored)', + fixture: 'multipleCreateAppCallsChainedFirst.ts', + expected: expect.objectContaining({kind: 'chain'}), + }, + ])('findMountAnchor should resolve $name', ({fixture, expected}) => { + const ast = loadFixture(fixture); + + expect(VuePluginCodemod.findMountAnchor(ast)).toEqual(expected); + }); + + it.each<{name: string, fixture: string, factoryLocal: string, expected: boolean}>([ + { + name: 'a variable-form anchor with the factory already registered', + fixture: 'alreadyRegistered.ts', + factoryLocal: 'createCroct', + expected: true, + }, + { + name: 'a chained anchor with the factory already registered', + fixture: 'chainedAlreadyRegistered.ts', + factoryLocal: 'createCroct', + expected: true, + }, + { + name: 'a variable-form anchor where the factory is imported but never used', + fixture: 'importedButNotUsed.ts', + factoryLocal: 'createCroct', + expected: false, + }, + { + name: 'a variable-form anchor where the factory is aliased and used', + fixture: 'aliasedFactoryImportAndUsed.ts', + factoryLocal: 'plug', + expected: true, + }, + { + name: 'a chained anchor without any use call', + fixture: 'chainedMount.ts', + factoryLocal: 'createCroct', + expected: false, + }, + { + name: 'a chained anchor walking past unrelated use calls', + fixture: 'chainedWithPlugins.ts', + factoryLocal: 'createCroct', + expected: false, + }, + { + name: 'a variable-form anchor without the factory', + fixture: 'variableForm.ts', + factoryLocal: 'createCroct', + expected: false, + }, + { + name: 'a variable-form anchor walking past unrelated use calls', + fixture: 'intermediateNonUseStatement.ts', + factoryLocal: 'createCroct', + expected: false, + }, + ])('hasFactoryCall should report $name', ({fixture, factoryLocal, expected}) => { + const ast = loadFixture(fixture); + const anchor = VuePluginCodemod.findMountAnchor(ast); + const result = anchor !== null && VuePluginCodemod.hasFactoryCall(anchor, factoryLocal); + + expect(result).toBe(expected); + }); + + it.each<{name: string, fixture: string, identifier: string, expected: object | null}>([ + { + name: 'a variable-form .use(StoryblokVue, ...) call', + fixture: '../vue-storyblok/storyblokBasicMigration.ts', + identifier: 'StoryblokVue', + expected: expect.objectContaining({type: 'CallExpression'}), + }, + { + name: 'a chained .use(StoryblokVue, ...) call', + fixture: '../vue-storyblok/storyblokChainedAppUse.ts', + identifier: 'StoryblokVue', + expected: expect.objectContaining({type: 'CallExpression'}), + }, + { + name: 'a missing identifier in variable form', + fixture: 'variableWithPlugins.ts', + identifier: 'NotPresent', + expected: null, + }, + { + name: 'a missing identifier in chain form', + fixture: 'chainedWithPlugins.ts', + identifier: 'NotPresent', + expected: null, + }, + ])('findUseCall should locate $name', ({fixture, identifier, expected}) => { + const ast = loadFixture(fixture); + const anchor = VuePluginCodemod.findMountAnchor(ast); + const useCall = anchor === null ? null : VuePluginCodemod.findUseCall(anchor, identifier); + + expect(useCall).toEqual(expected); + }); +}); diff --git a/test/application/project/code/transformation/javascript/vueStoryblokCodemod.test.ts b/test/application/project/code/transformation/javascript/vueStoryblokCodemod.test.ts new file mode 100644 index 00000000..871a83f8 --- /dev/null +++ b/test/application/project/code/transformation/javascript/vueStoryblokCodemod.test.ts @@ -0,0 +1,35 @@ +import {resolve} from 'path'; +import type {VueStoryblokConfiguration} from '@/application/project/code/transformation/javascript/vueStoryblokCodemod'; +import {VueStoryblokCodemod} from '@/application/project/code/transformation/javascript/vueStoryblokCodemod'; +import {JavaScriptCodemod} from '@/application/project/code/transformation/javascript/javaScriptCodemod'; +import {loadFixtures} from '../fixtures'; + +describe('VueStoryblokCodemod', () => { + const defaultOptions: VueStoryblokConfiguration = { + plugin: { + module: '@croct/plug-storyblok/vue', + factory: 'withCroct', + }, + storyblok: { + module: '@storyblok/vue', + identifier: 'StoryblokVue', + }, + }; + + const scenarios = loadFixtures( + resolve(__dirname, '../fixtures/vue-storyblok'), + defaultOptions, + {}, + ); + + it.each(scenarios)('should correctly transform $name', async ({name, fixture, options}) => { + const transformer = new JavaScriptCodemod({ + languages: ['typescript', 'jsx'], + codemod: new VueStoryblokCodemod(options), + }); + + const output = await transformer.apply(fixture); + + expect(output.result).toMatchSnapshot(name); + }); +}); From b34f5170102b96b892a1faeaaef7ffb4a27676bb Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Thu, 28 May 2026 11:58:45 -0300 Subject: [PATCH 2/9] Add code mods for Vue and Nuxt --- .../code/transformation/javascript/vuePluginCodemod.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/application/project/code/transformation/javascript/vuePluginCodemod.test.ts b/test/application/project/code/transformation/javascript/vuePluginCodemod.test.ts index 804f4da9..f2285439 100644 --- a/test/application/project/code/transformation/javascript/vuePluginCodemod.test.ts +++ b/test/application/project/code/transformation/javascript/vuePluginCodemod.test.ts @@ -21,7 +21,7 @@ describe('VuePluginCodemod', () => { }; function loadFixture(name: string): t.File { - return parse(readFileSync(resolve(fixturesPath, name), 'utf-8'), ['typescript', 'jsx']); + return parse(readFileSync(resolve(fixturesPath, name), 'utf-8'), ['typescript']); } it.each( @@ -70,7 +70,7 @@ describe('VuePluginCodemod', () => { ), )('apply should correctly transform $name', async ({name, fixture, options}) => { const transformer = new JavaScriptCodemod({ - languages: ['typescript', 'jsx'], + languages: ['typescript'], codemod: new VuePluginCodemod(options), }); From d60b6970744e61c0f81d508188f2f5fe64d689cb Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Thu, 28 May 2026 11:58:54 -0300 Subject: [PATCH 3/9] Add code mods for Vue and Nuxt --- .../transformation/javascript/nuxtConfigModuleCodemod.test.ts | 2 +- .../javascript/nuxtStoryblokPluginCodemod.test.ts | 2 +- .../code/transformation/javascript/vueStoryblokCodemod.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.test.ts b/test/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.test.ts index aed76599..3a1f63e2 100644 --- a/test/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.test.ts +++ b/test/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.test.ts @@ -19,7 +19,7 @@ describe('NuxtConfigModuleCodemod', () => { it.each(scenarios)('should correctly transform $name', async ({name, fixture, options}) => { const transformer = new JavaScriptCodemod({ - languages: ['typescript', 'jsx'], + languages: ['typescript'], codemod: new NuxtConfigModuleCodemod(options), }); diff --git a/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts b/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts index bdd6b8f5..20e68e05 100644 --- a/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts +++ b/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts @@ -26,7 +26,7 @@ describe('NuxtStoryblokPluginCodemod', () => { it.each(scenarios)('should correctly transform $name', async ({name, fixture, options}) => { const transformer = new JavaScriptCodemod({ - languages: ['typescript', 'jsx'], + languages: ['typescript'], codemod: new NuxtStoryblokPluginCodemod(options), }); diff --git a/test/application/project/code/transformation/javascript/vueStoryblokCodemod.test.ts b/test/application/project/code/transformation/javascript/vueStoryblokCodemod.test.ts index 871a83f8..2e97491d 100644 --- a/test/application/project/code/transformation/javascript/vueStoryblokCodemod.test.ts +++ b/test/application/project/code/transformation/javascript/vueStoryblokCodemod.test.ts @@ -24,7 +24,7 @@ describe('VueStoryblokCodemod', () => { it.each(scenarios)('should correctly transform $name', async ({name, fixture, options}) => { const transformer = new JavaScriptCodemod({ - languages: ['typescript', 'jsx'], + languages: ['typescript'], codemod: new VueStoryblokCodemod(options), }); From 7c8e7a66a87bbf861e55e98bee96dc78a18b6444 Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Thu, 28 May 2026 12:52:23 -0300 Subject: [PATCH 4/9] Add example generators --- .../project/code/generation/example.ts | 1 + .../slot/plugNuxtExampleGenerator.ts | 20 + .../slot/plugVueExampleGenerator.ts | 31 + .../generation/slot/vueExampleGenerator.ts | 302 +++++++ .../plugNuxtExampleGenerator.test.ts.snap | 635 ++++++++++++++ .../plugVueExampleGenerator.test.ts.snap | 815 ++++++++++++++++++ .../slot/fixtures/booleanWithLabel.json | 25 + .../slot/fixtures/labeledAttributes.json | 16 + .../slot/fixtures/listOfScalars.json | 18 + .../slot/fixtures/listOfStructures.json | 33 + .../slot/fixtures/nestedStructure.json | 30 + .../slot/fixtures/optionalAttributes.json | 22 + .../slot/fixtures/privateAttributes.json | 22 + .../slot/fixtures/simpleStructure.json | 27 + .../slot/fixtures/unionInStructure.json | 39 + .../generation/slot/fixtures/unionRoot.json | 37 + .../slot/plugNuxtExampleGenerator.test.ts | 56 ++ .../slot/plugVueExampleGenerator.test.ts | 56 ++ 18 files changed, 2185 insertions(+) create mode 100644 src/application/project/code/generation/slot/plugNuxtExampleGenerator.ts create mode 100644 src/application/project/code/generation/slot/plugVueExampleGenerator.ts create mode 100644 src/application/project/code/generation/slot/vueExampleGenerator.ts create mode 100644 test/application/project/code/generation/slot/__snapshots__/plugNuxtExampleGenerator.test.ts.snap create mode 100644 test/application/project/code/generation/slot/__snapshots__/plugVueExampleGenerator.test.ts.snap create mode 100644 test/application/project/code/generation/slot/fixtures/booleanWithLabel.json create mode 100644 test/application/project/code/generation/slot/fixtures/labeledAttributes.json create mode 100644 test/application/project/code/generation/slot/fixtures/listOfScalars.json create mode 100644 test/application/project/code/generation/slot/fixtures/listOfStructures.json create mode 100644 test/application/project/code/generation/slot/fixtures/nestedStructure.json create mode 100644 test/application/project/code/generation/slot/fixtures/optionalAttributes.json create mode 100644 test/application/project/code/generation/slot/fixtures/privateAttributes.json create mode 100644 test/application/project/code/generation/slot/fixtures/simpleStructure.json create mode 100644 test/application/project/code/generation/slot/fixtures/unionInStructure.json create mode 100644 test/application/project/code/generation/slot/fixtures/unionRoot.json create mode 100644 test/application/project/code/generation/slot/plugNuxtExampleGenerator.test.ts create mode 100644 test/application/project/code/generation/slot/plugVueExampleGenerator.test.ts diff --git a/src/application/project/code/generation/example.ts b/src/application/project/code/generation/example.ts index 6d871fee..2d891d28 100644 --- a/src/application/project/code/generation/example.ts +++ b/src/application/project/code/generation/example.ts @@ -4,6 +4,7 @@ export enum CodeLanguage { JAVASCRIPT_XML = 'jsx', TYPESCRIPT = 'typescript', TYPESCRIPT_XML = 'tsx', + VUE = 'vue', } export type ExampleFile = { diff --git a/src/application/project/code/generation/slot/plugNuxtExampleGenerator.ts b/src/application/project/code/generation/slot/plugNuxtExampleGenerator.ts new file mode 100644 index 00000000..c236cf11 --- /dev/null +++ b/src/application/project/code/generation/slot/plugNuxtExampleGenerator.ts @@ -0,0 +1,20 @@ +import {VueExampleGenerator} from './vueExampleGenerator'; +import type {SlotDefinition} from './slotExampleGenerator'; +import type {CodeWriter} from '@/application/project/code/generation/codeWritter'; + +export class PlugNuxtExampleGenerator extends VueExampleGenerator { + protected writeSlotScript(writer: CodeWriter, definition: SlotDefinition): void { + const variable = this.options.contentVariable; + const binding = variable === 'data' ? variable : `data: ${variable}`; + + this.writeScriptOpening(writer); + + writer.write(`const {${binding}} = await useContent('${definition.id}@${definition.version}');`); + + this.writeScriptClosing(writer); + } + + protected writePageScript(): void { + // Nuxt auto-imports components from `components/`, so the page needs no script section. + } +} diff --git a/src/application/project/code/generation/slot/plugVueExampleGenerator.ts b/src/application/project/code/generation/slot/plugVueExampleGenerator.ts new file mode 100644 index 00000000..db248d38 --- /dev/null +++ b/src/application/project/code/generation/slot/plugVueExampleGenerator.ts @@ -0,0 +1,31 @@ +import {VueExampleGenerator} from './vueExampleGenerator'; +import type {SlotFile} from './vueExampleGenerator'; +import type {SlotDefinition} from './slotExampleGenerator'; +import type {CodeWriter} from '@/application/project/code/generation/codeWritter'; + +export class PlugVueExampleGenerator extends VueExampleGenerator { + protected writeSlotScript(writer: CodeWriter, definition: SlotDefinition): void { + const variable = this.options.contentVariable; + const dataBinding = variable === 'data' ? variable : `data: ${variable}`; + + this.writeScriptOpening(writer); + + writer.write("import {useContent} from '@croct/plug-vue';") + .newLine() + .write(`const {${dataBinding}, isLoading} = useContent('${definition.id}@${definition.version}');`); + + this.writeScriptClosing(writer); + } + + protected writePageScript(writer: CodeWriter, slot: SlotFile): void { + this.writeScriptOpening(writer); + + writer.write(`import ${slot.name} from '${slot.importPath}';`); + + this.writeScriptClosing(writer); + } + + protected override getLoadingFlag(): string { + return 'isLoading'; + } +} diff --git a/src/application/project/code/generation/slot/vueExampleGenerator.ts b/src/application/project/code/generation/slot/vueExampleGenerator.ts new file mode 100644 index 00000000..5882de19 --- /dev/null +++ b/src/application/project/code/generation/slot/vueExampleGenerator.ts @@ -0,0 +1,302 @@ +import type { + AttributeDefinition, + ContentDefinition, + RootDefinition, +} from '@croct/content-model/definition/definition'; +import type {SlotDefinition, SlotExampleGenerator} from './slotExampleGenerator'; +import type {CodeExample, ExampleFile} from '@/application/project/code/generation/example'; +import {CodeLanguage} from '@/application/project/code/generation/example'; +import {CodeWriter} from '@/application/project/code/generation/codeWritter'; +import {formatLabel, formatSlug, sortAttributes} from '@/application/project/code/generation/utils'; +import {formatName} from '@/application/project/utils/formatName'; + +export type Configuration = { + typescript: boolean, + indentationSize?: number, + contentVariable: string, + slotImportPath: string, + slotFilePath: string, + slotComponentName: string, + pageFilePath: string, +}; + +export type SlotFile = { + name: string, + path: string, + importPath: string, + definition: SlotDefinition, +}; + +type Attribute = AttributeDefinition & { + name: string, +}; + +export abstract class VueExampleGenerator implements SlotExampleGenerator { + protected readonly options: Configuration; + + public constructor(options: Configuration) { + this.options = options; + } + + public generate(definition: SlotDefinition): CodeExample { + const slotPath = VueExampleGenerator.replaceVariables(this.options.slotFilePath, definition.id); + const slotName = VueExampleGenerator.replaceVariables(this.options.slotComponentName, definition.id); + + return { + files: [ + this.generatePageFile(definition, { + name: slotName, + path: slotPath, + importPath: VueExampleGenerator.replaceVariables(this.options.slotImportPath, definition.id), + definition: definition, + }), + this.generateSlotFile(definition, slotPath), + ], + }; + } + + protected abstract writeSlotScript(writer: CodeWriter, definition: SlotDefinition): void; + + protected abstract writePageScript(writer: CodeWriter, slot: SlotFile): void; + + protected getLoadingFlag(): string | null { + return null; + } + + private generateSlotFile(definition: SlotDefinition, path: string): ExampleFile { + const writer = this.createWriter(); + const variable = this.options.contentVariable; + const loadingFlag = this.getLoadingFlag(); + + this.writeSlotScript(writer, definition); + + writer.newLine(); + writer.write('', false); + + return { + path: path, + language: CodeLanguage.VUE, + code: writer.toString(), + }; + } + + private generatePageFile(definition: SlotDefinition, slot: SlotFile): ExampleFile { + const writer = this.createWriter(); + const pagePath = VueExampleGenerator.replaceVariables(this.options.pageFilePath, definition.id); + + this.writePageScript(writer, slot); + + if (writer.toString() !== '') { + writer.newLine(); + } + + writer.write('', false); + + return { + path: pagePath, + language: CodeLanguage.VUE, + code: writer.toString(), + }; + } + + protected writeScriptOpening(writer: CodeWriter): void { + writer.write(this.options.typescript ? ''); + } + + private writeRoot(writer: CodeWriter, definition: RootDefinition, path: string): void { + if (definition.type === 'union') { + this.writeUnion(writer, definition, path); + + return; + } + + writer.write(`
    `) + .indent(); + + this.writeStructureAttributes(writer, definition, path); + + writer.outdent() + .write('
'); + } + + private writeFragment(writer: CodeWriter, definition: ContentDefinition, path: string): void { + switch (definition.type) { + case 'text': + case 'number': + writer.append(`{{ ${path} }}`); + + break; + + case 'boolean': + writer.append(VueExampleGenerator.formatBooleanInterpolation(definition, path)); + + break; + + case 'list': + this.writeList(writer, definition, path); + + break; + + case 'structure': + writer.write('
    ') + .indent(); + + this.writeStructureAttributes(writer, definition, path); + + writer.outdent() + .write('
'); + + break; + + case 'union': + this.writeUnion(writer, definition, path); + + break; + } + } + + private writeList(writer: CodeWriter, definition: ContentDefinition<'list'>, path: string): void { + const variable = definition.itemLabel !== undefined + ? formatName(definition.itemLabel) + : 'item'; + + writer.write('
    ') + .indent() + .write(`
  1. `) + .indent(); + + const inline = VueExampleGenerator.isInline(definition.items); + + if (inline) { + writer.appendIndentation(); + } + + this.writeFragment(writer, definition.items, variable); + + if (inline) { + writer.newLine(); + } + + writer.outdent() + .write('
  2. ') + .outdent() + .write('
'); + } + + private writeStructureAttributes( + writer: CodeWriter, + definition: ContentDefinition<'structure'>, + path: string, + ): void { + for (const [name, attribute] of sortAttributes(definition.attributes)) { + if (attribute.private === true) { + continue; + } + + this.writeAttribute(writer, {name: name, ...attribute}, path); + } + } + + private writeAttribute(writer: CodeWriter, attribute: Attribute, parentPath: string): void { + const path = `${parentPath}.${attribute.name}`; + const definition = attribute.type; + const label = VueExampleGenerator.escapeEntities(attribute.label ?? formatLabel(attribute.name)); + const guard = attribute.optional === true ? ` v-if="${path}"` : ''; + + switch (definition.type) { + case 'boolean': + case 'text': + case 'number': { + writer.write(``, false) + .append(`${label}: `); + + this.writeFragment(writer, definition, path); + + writer.append('') + .newLine(); + + break; + } + + default: + writer.write(``) + .indent() + .write(`${label}`); + + this.writeFragment(writer, definition, path); + + writer.outdent() + .write(''); + + break; + } + } + + private writeUnion(writer: CodeWriter, definition: ContentDefinition<'union'>, path: string): void { + for (const [id, variant] of Object.entries(definition.types)) { + writer.write(`'); + } + } + + private static formatBooleanInterpolation( + definition: ContentDefinition<'boolean'>, + path: string, + ): string { + const trueLabel = definition.label?.true ?? 'Yes'; + const falseLabel = definition.label?.false ?? 'No'; + + return `{{ ${path} ? '${trueLabel.replace(/'/g, "\\'")}' : '${falseLabel.replace(/'/g, "\\'")}' }}`; + } + + private static isInline(definition: ContentDefinition): boolean { + return ['number', 'text', 'boolean'].includes(definition.type); + } + + private static escapeEntities(value: string): string { + return value + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + } + + private createWriter(): CodeWriter { + return new CodeWriter(this.options.indentationSize); + } + + private static replaceVariables(path: string, id: string): string { + return path.replace(/%name%/g, CodeWriter.formatName(id, true)) + .replace(/%slug%/g, formatSlug(id)); + } +} diff --git a/test/application/project/code/generation/slot/__snapshots__/plugNuxtExampleGenerator.test.ts.snap b/test/application/project/code/generation/slot/__snapshots__/plugNuxtExampleGenerator.test.ts.snap new file mode 100644 index 00000000..f9ea798c --- /dev/null +++ b/test/application/project/code/generation/slot/__snapshots__/plugNuxtExampleGenerator.test.ts.snap @@ -0,0 +1,635 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for booleanWithLabel: booleanWithLabel-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/feature-toggle/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/feature-toggle.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for labeledAttributes: labeledAttributes-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/stylized/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/stylized.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for listOfScalars: listOfScalars-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/tag-cloud/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/tag-cloud.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for listOfStructures: listOfStructures-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/product-grid/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/product-grid.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for nestedStructure: nestedStructure-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/author-card/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/author-card.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for optionalAttributes: optionalAttributes-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/promo-card/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/promo-card.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for privateAttributes: privateAttributes-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/tracked-banner/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/tracked-banner.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for simpleStructure: simpleStructure-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/home-hero/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/home-hero.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for unionInStructure: unionInStructure-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/block/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/block.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for unionRoot: unionRoot-js 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/cta-block/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/cta-block.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for booleanWithLabel: booleanWithLabel 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/feature-toggle/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/feature-toggle.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for labeledAttributes: labeledAttributes 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/stylized/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/stylized.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for listOfScalars: listOfScalars 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/tag-cloud/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/tag-cloud.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for listOfStructures: listOfStructures 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/product-grid/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/product-grid.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for nestedStructure: nestedStructure 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/author-card/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/author-card.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for optionalAttributes: optionalAttributes 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/promo-card/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/promo-card.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for privateAttributes: privateAttributes 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/tracked-banner/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/tracked-banner.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for simpleStructure: simpleStructure 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/home-hero/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/home-hero.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for unionInStructure: unionInStructure 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/block/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/block.vue", + }, + ], +} +`; + +exports[`PlugNuxtExampleGenerator should generate TypeScript SFCs for unionRoot: unionRoot 1`] = ` +{ + "files": [ + { + "code": "", + "language": "vue", + "path": "pages/cta-block/index.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "components/cta-block.vue", + }, + ], +} +`; diff --git a/test/application/project/code/generation/slot/__snapshots__/plugVueExampleGenerator.test.ts.snap b/test/application/project/code/generation/slot/__snapshots__/plugVueExampleGenerator.test.ts.snap new file mode 100644 index 00000000..ecd0c6aa --- /dev/null +++ b/test/application/project/code/generation/slot/__snapshots__/plugVueExampleGenerator.test.ts.snap @@ -0,0 +1,815 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for booleanWithLabel: booleanWithLabel-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/feature-toggle-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/feature-toggle.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for labeledAttributes: labeledAttributes-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/stylized-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/stylized.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for listOfScalars: listOfScalars-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/tag-cloud-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/tag-cloud.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for listOfStructures: listOfStructures-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/product-grid-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/product-grid.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for nestedStructure: nestedStructure-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/author-card-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/author-card.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for optionalAttributes: optionalAttributes-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/promo-card-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/promo-card.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for privateAttributes: privateAttributes-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/tracked-banner-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/tracked-banner.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for simpleStructure: simpleStructure-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/home-hero-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/home-hero.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for unionInStructure: unionInStructure-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/block-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/block.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate JavaScript SFCs for unionRoot: unionRoot-js 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/cta-block-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/cta-block.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for booleanWithLabel: booleanWithLabel 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/feature-toggle-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/feature-toggle.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for labeledAttributes: labeledAttributes 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/stylized-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/stylized.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for listOfScalars: listOfScalars 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/tag-cloud-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/tag-cloud.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for listOfStructures: listOfStructures 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/product-grid-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/product-grid.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for nestedStructure: nestedStructure 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/author-card-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/author-card.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for optionalAttributes: optionalAttributes 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/promo-card-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/promo-card.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for privateAttributes: privateAttributes 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/tracked-banner-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/tracked-banner.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for simpleStructure: simpleStructure 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/home-hero-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/home-hero.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for unionInStructure: unionInStructure 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/block-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/block.vue", + }, + ], +} +`; + +exports[`PlugVueExampleGenerator should generate TypeScript SFCs for unionRoot: unionRoot 1`] = ` +{ + "files": [ + { + "code": " + +", + "language": "vue", + "path": "src/examples/cta-block-example.vue", + }, + { + "code": " + +", + "language": "vue", + "path": "src/components/cta-block.vue", + }, + ], +} +`; diff --git a/test/application/project/code/generation/slot/fixtures/booleanWithLabel.json b/test/application/project/code/generation/slot/fixtures/booleanWithLabel.json new file mode 100644 index 00000000..2e783d08 --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/booleanWithLabel.json @@ -0,0 +1,25 @@ +{ + "id": "feature-toggle", + "version": 1, + "definition": { + "type": "structure", + "attributes": { + "enabled": { + "type": { + "type": "boolean", + "label": { + "true": "Active", + "false": "Inactive" + } + }, + "position": 0 + }, + "visible": { + "type": { + "type": "boolean" + }, + "position": 1 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/labeledAttributes.json b/test/application/project/code/generation/slot/fixtures/labeledAttributes.json new file mode 100644 index 00000000..d0ac73d0 --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/labeledAttributes.json @@ -0,0 +1,16 @@ +{ + "id": "stylized", + "version": 1, + "definition": { + "type": "structure", + "attributes": { + "title": { + "type": { + "type": "text" + }, + "label": "Tom & Jerry ", + "position": 0 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/listOfScalars.json b/test/application/project/code/generation/slot/fixtures/listOfScalars.json new file mode 100644 index 00000000..d3e4b74b --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/listOfScalars.json @@ -0,0 +1,18 @@ +{ + "id": "tag-cloud", + "version": 1, + "definition": { + "type": "structure", + "attributes": { + "tags": { + "type": { + "type": "list", + "items": { + "type": "text" + } + }, + "position": 0 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/listOfStructures.json b/test/application/project/code/generation/slot/fixtures/listOfStructures.json new file mode 100644 index 00000000..08ab6cd5 --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/listOfStructures.json @@ -0,0 +1,33 @@ +{ + "id": "product-grid", + "version": 3, + "definition": { + "type": "structure", + "attributes": { + "products": { + "type": { + "type": "list", + "itemLabel": "Product", + "items": { + "type": "structure", + "attributes": { + "name": { + "type": { + "type": "text" + }, + "position": 0 + }, + "price": { + "type": { + "type": "number" + }, + "position": 1 + } + } + } + }, + "position": 0 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/nestedStructure.json b/test/application/project/code/generation/slot/fixtures/nestedStructure.json new file mode 100644 index 00000000..33bf68af --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/nestedStructure.json @@ -0,0 +1,30 @@ +{ + "id": "author-card", + "version": 1, + "definition": { + "type": "structure", + "attributes": { + "author": { + "type": { + "type": "structure", + "attributes": { + "name": { + "type": { + "type": "text" + }, + "position": 0 + }, + "bio": { + "type": { + "type": "text" + }, + "optional": true, + "position": 1 + } + } + }, + "position": 0 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/optionalAttributes.json b/test/application/project/code/generation/slot/fixtures/optionalAttributes.json new file mode 100644 index 00000000..83ae9baa --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/optionalAttributes.json @@ -0,0 +1,22 @@ +{ + "id": "promo-card", + "version": 2, + "definition": { + "type": "structure", + "attributes": { + "title": { + "type": { + "type": "text" + }, + "position": 0 + }, + "subtitle": { + "type": { + "type": "text" + }, + "optional": true, + "position": 1 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/privateAttributes.json b/test/application/project/code/generation/slot/fixtures/privateAttributes.json new file mode 100644 index 00000000..79c18578 --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/privateAttributes.json @@ -0,0 +1,22 @@ +{ + "id": "tracked-banner", + "version": 1, + "definition": { + "type": "structure", + "attributes": { + "headline": { + "type": { + "type": "text" + }, + "position": 0 + }, + "trackingId": { + "type": { + "type": "text" + }, + "private": true, + "position": 1 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/simpleStructure.json b/test/application/project/code/generation/slot/fixtures/simpleStructure.json new file mode 100644 index 00000000..ee2907ae --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/simpleStructure.json @@ -0,0 +1,27 @@ +{ + "id": "home-hero", + "version": 1, + "definition": { + "type": "structure", + "attributes": { + "headline": { + "type": { + "type": "text" + }, + "position": 0 + }, + "viewCount": { + "type": { + "type": "number" + }, + "position": 1 + }, + "featured": { + "type": { + "type": "boolean" + }, + "position": 2 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/unionInStructure.json b/test/application/project/code/generation/slot/fixtures/unionInStructure.json new file mode 100644 index 00000000..760c4246 --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/unionInStructure.json @@ -0,0 +1,39 @@ +{ + "id": "block", + "version": 1, + "definition": { + "type": "structure", + "attributes": { + "media": { + "type": { + "type": "union", + "types": { + "image": { + "type": "structure", + "attributes": { + "url": { + "type": { + "type": "text" + }, + "position": 0 + } + } + }, + "video": { + "type": "structure", + "attributes": { + "source": { + "type": { + "type": "text" + }, + "position": 0 + } + } + } + } + }, + "position": 0 + } + } + } +} diff --git a/test/application/project/code/generation/slot/fixtures/unionRoot.json b/test/application/project/code/generation/slot/fixtures/unionRoot.json new file mode 100644 index 00000000..37ea9714 --- /dev/null +++ b/test/application/project/code/generation/slot/fixtures/unionRoot.json @@ -0,0 +1,37 @@ +{ + "id": "cta-block", + "version": 1, + "definition": { + "type": "union", + "types": { + "button": { + "type": "structure", + "attributes": { + "label": { + "type": { + "type": "text" + }, + "position": 0 + } + } + }, + "link": { + "type": "structure", + "attributes": { + "url": { + "type": { + "type": "text" + }, + "position": 0 + }, + "text": { + "type": { + "type": "text" + }, + "position": 1 + } + } + } + } + } +} diff --git a/test/application/project/code/generation/slot/plugNuxtExampleGenerator.test.ts b/test/application/project/code/generation/slot/plugNuxtExampleGenerator.test.ts new file mode 100644 index 00000000..650a35bd --- /dev/null +++ b/test/application/project/code/generation/slot/plugNuxtExampleGenerator.test.ts @@ -0,0 +1,56 @@ +import {readFileSync, readdirSync} from 'fs'; +import {basename, resolve} from 'path'; +import type {SlotDefinition} from '@/application/project/code/generation/slot/slotExampleGenerator'; +import type {Configuration} from '@/application/project/code/generation/slot/vueExampleGenerator'; +import {PlugNuxtExampleGenerator} from '@/application/project/code/generation/slot/plugNuxtExampleGenerator'; + +describe('PlugNuxtExampleGenerator', () => { + const fixturesPath = resolve(__dirname, 'fixtures'); + + function loadFixture(file: string): SlotDefinition { + return JSON.parse(readFileSync(resolve(fixturesPath, file), 'utf-8')); + } + + const fixtures = readdirSync(fixturesPath).filter(file => file.endsWith('.json')) + .map( + file => ({ + name: basename(file, '.json'), + definition: loadFixture(file), + }), + ); + + const tsOptions: Configuration = { + typescript: true, + contentVariable: 'content', + slotImportPath: '~/components/%slug%.vue', + slotFilePath: 'components/%slug%.vue', + slotComponentName: '%name%', + pageFilePath: 'pages/%slug%/index.vue', + }; + + it.each(fixtures)('should generate TypeScript SFCs for $name', ({name, definition}) => { + const example = new PlugNuxtExampleGenerator(tsOptions).generate(definition); + + expect(example).toMatchSnapshot(name); + }); + + it.each(fixtures)('should generate JavaScript SFCs for $name', ({name, definition}) => { + const example = new PlugNuxtExampleGenerator({...tsOptions, typescript: false}).generate(definition); + + expect(example).toMatchSnapshot(`${name}-js`); + }); + + it('should use a custom content variable binding', () => { + const example = new PlugNuxtExampleGenerator({...tsOptions, contentVariable: 'props'}) + .generate(loadFixture('simpleStructure.json')); + + expect(example.files[1].code).toContain("const {data: props} = await useContent('home-hero@1');"); + }); + + it('should not rebind the destructured variable when it is named "data"', () => { + const example = new PlugNuxtExampleGenerator({...tsOptions, contentVariable: 'data'}) + .generate(loadFixture('simpleStructure.json')); + + expect(example.files[1].code).toContain("const {data} = await useContent('home-hero@1');"); + }); +}); diff --git a/test/application/project/code/generation/slot/plugVueExampleGenerator.test.ts b/test/application/project/code/generation/slot/plugVueExampleGenerator.test.ts new file mode 100644 index 00000000..bc9dcd35 --- /dev/null +++ b/test/application/project/code/generation/slot/plugVueExampleGenerator.test.ts @@ -0,0 +1,56 @@ +import {readFileSync, readdirSync} from 'fs'; +import {basename, resolve} from 'path'; +import type {SlotDefinition} from '@/application/project/code/generation/slot/slotExampleGenerator'; +import type {Configuration} from '@/application/project/code/generation/slot/vueExampleGenerator'; +import {PlugVueExampleGenerator} from '@/application/project/code/generation/slot/plugVueExampleGenerator'; + +describe('PlugVueExampleGenerator', () => { + const fixturesPath = resolve(__dirname, 'fixtures'); + + function loadFixture(file: string): SlotDefinition { + return JSON.parse(readFileSync(resolve(fixturesPath, file), 'utf-8')); + } + + const fixtures = readdirSync(fixturesPath).filter(file => file.endsWith('.json')) + .map( + file => ({ + name: basename(file, '.json'), + definition: loadFixture(file), + }), + ); + + const tsOptions: Configuration = { + typescript: true, + contentVariable: 'content', + slotImportPath: './%slug%.vue', + slotFilePath: 'src/components/%slug%.vue', + slotComponentName: '%name%', + pageFilePath: 'src/examples/%slug%-example.vue', + }; + + it.each(fixtures)('should generate TypeScript SFCs for $name', ({name, definition}) => { + const example = new PlugVueExampleGenerator(tsOptions).generate(definition); + + expect(example).toMatchSnapshot(name); + }); + + it.each(fixtures)('should generate JavaScript SFCs for $name', ({name, definition}) => { + const example = new PlugVueExampleGenerator({...tsOptions, typescript: false}).generate(definition); + + expect(example).toMatchSnapshot(`${name}-js`); + }); + + it('should use a custom content variable binding', () => { + const example = new PlugVueExampleGenerator({...tsOptions, contentVariable: 'props'}) + .generate(loadFixture('simpleStructure.json')); + + expect(example.files[1].code).toContain("const {data: props, isLoading} = useContent('home-hero@1');"); + }); + + it('should not rebind the destructured variable when it is named "data"', () => { + const example = new PlugVueExampleGenerator({...tsOptions, contentVariable: 'data'}) + .generate(loadFixture('simpleStructure.json')); + + expect(example.files[1].code).toContain("const {data, isLoading} = useContent('home-hero@1');"); + }); +}); From 68f659af23f1294062f83c0652321afcd671474c Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Thu, 28 May 2026 13:06:17 -0300 Subject: [PATCH 5/9] Add sdks --- .../javascript/vuePluginCodemod.ts | 11 +- src/application/project/sdk/plugNuxtSdk.ts | 264 ++++++++++++++++ src/application/project/sdk/plugVueSdk.ts | 295 ++++++++++++++++++ 3 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 src/application/project/sdk/plugNuxtSdk.ts create mode 100644 src/application/project/sdk/plugVueSdk.ts diff --git a/src/application/project/code/transformation/javascript/vuePluginCodemod.ts b/src/application/project/code/transformation/javascript/vuePluginCodemod.ts index b03f7102..4b5fdbe3 100644 --- a/src/application/project/code/transformation/javascript/vuePluginCodemod.ts +++ b/src/application/project/code/transformation/javascript/vuePluginCodemod.ts @@ -36,6 +36,10 @@ export type VuePluginConfiguration = { args?: Record, }; +export type VuePluginOptions = CodemodOptions & { + args?: Record, +}; + /** * Registers a Vue plugin in the main entry file. * @@ -43,21 +47,22 @@ export type VuePluginConfiguration = { * form the user wrote (chained or variable). If the plugin is already * registered, the codemod returns unmodified. */ -export class VuePluginCodemod implements Codemod { +export class VuePluginCodemod implements Codemod { private readonly configuration: VuePluginConfiguration; public constructor(configuration: VuePluginConfiguration) { this.configuration = configuration; } - public apply(input: t.File): Promise> { + public apply(input: t.File, options: VuePluginOptions = {}): Promise> { const anchor = VuePluginCodemod.findMountAnchor(input); if (anchor === null) { return Promise.resolve({modified: false, result: input}); } - const {plugin, args} = this.configuration; + const {plugin} = this.configuration; + const args = options.args ?? this.configuration.args; const factoryLocal = getImportLocalName(input, { moduleName: plugin.module, diff --git a/src/application/project/sdk/plugNuxtSdk.ts b/src/application/project/sdk/plugNuxtSdk.ts new file mode 100644 index 00000000..7bfbd5bf --- /dev/null +++ b/src/application/project/sdk/plugNuxtSdk.ts @@ -0,0 +1,264 @@ +import type {Installation} from '@/application/project/sdk/sdk'; +import {SdkError} from '@/application/project/sdk/sdk'; +import type { + Configuration as JavaScriptSdkConfiguration, + InstallationPlan, +} from '@/application/project/sdk/javasScriptSdk'; +import {JavaScriptSdk} from '@/application/project/sdk/javasScriptSdk'; +import type {ApplicationApi, GeneratedApiKey} from '@/application/api/application'; +import type {WorkspaceApi} from '@/application/api/workspace'; +import {EnvFile} from '@/application/project/code/envFile'; +import type {UserApi} from '@/application/api/user'; +import type {Codemod} from '@/application/project/code/transformation/codemod'; +import type {Task, TaskNotifier} from '@/application/cli/io/output'; +import type {ExampleFile} from '@/application/project/code/generation/example'; +import {PlugNuxtExampleGenerator} from '@/application/project/code/generation/slot/plugNuxtExampleGenerator'; +import {ApiError} from '@/application/api/error'; +import type {Slot} from '@/application/model/slot'; +import {ErrorReason, HelpfulError} from '@/application/error'; +import {ApiKeyPermission} from '@/application/model/application'; + +type CodemodConfiguration = { + config: Codemod, +}; + +export type Configuration = JavaScriptSdkConfiguration & { + codemod: CodemodConfiguration, + userApi: UserApi, + workspaceApi: WorkspaceApi, + applicationApi: ApplicationApi, +}; + +type NuxtProjectInfo = { + typescript: boolean, + config: { + file: string, + }, + env: { + localFile: EnvFile, + developmentFile: EnvFile, + productionFile: EnvFile, + }, +}; + +type NuxtInstallation = Installation & { + project: NuxtProjectInfo, + notifier: TaskNotifier, +}; + +enum NuxtEnvVar { + API_KEY = 'NUXT_CROCT_API_KEY', + APP_ID = 'NUXT_PUBLIC_CROCT_APP_ID', +} + +export class PlugNuxtSdk extends JavaScriptSdk { + private readonly userApi: UserApi; + + private readonly applicationApi: ApplicationApi; + + private readonly codemod: CodemodConfiguration; + + public constructor(configuration: Configuration) { + super(configuration); + + this.codemod = configuration.codemod; + this.userApi = configuration.userApi; + this.applicationApi = configuration.applicationApi; + } + + protected async generateSlotExampleFiles(slot: Slot, installation: Installation): Promise { + const isTypeScript = await this.isTypeScriptProject(); + + const paths = await this.getPaths(installation.configuration); + const slotPath = this.fileSystem.joinPaths(paths.components, '%slug%.vue'); + const pagePath = this.fileSystem.joinPaths(paths.examples, '%slug%', 'index.vue'); + + const generator = new PlugNuxtExampleGenerator({ + typescript: isTypeScript, + contentVariable: 'content', + slotImportPath: this.fileSystem.joinPaths('~', paths.components, '%slug%.vue'), + slotFilePath: slotPath, + slotComponentName: '%name%', + pageFilePath: pagePath, + }); + + const example = generator.generate({ + id: slot.slug, + version: slot.version.major, + definition: slot.resolvedDefinition, + }); + + return example.files; + } + + protected async getInstallationPlan(installation: Installation): Promise { + const {configuration} = installation; + const projectInfo = await this.getProjectInfo(); + + return { + dependencies: ['@croct/plug-nuxt'], + tasks: this.getInstallationTasks({ + ...installation, + project: projectInfo, + }), + configuration: { + ...configuration, + paths: { + ...configuration.paths, + examples: 'pages', + }, + }, + }; + } + + private async getProjectInfo(): Promise { + const [isTypescript, configFile] = await Promise.all([ + this.isTypeScriptProject(), + this.locateNuxtConfig(), + ]); + + const projectDirectory = this.projectDirectory.get(); + const extension = isTypescript ? 'ts' : 'js'; + + return { + typescript: isTypescript, + config: { + file: configFile ?? `nuxt.config.${extension}`, + }, + env: { + localFile: new EnvFile( + this.fileSystem, + this.fileSystem.joinPaths(projectDirectory, '.env.local'), + ), + developmentFile: new EnvFile( + this.fileSystem, + this.fileSystem.joinPaths(projectDirectory, '.env.development'), + ), + productionFile: new EnvFile( + this.fileSystem, + this.fileSystem.joinPaths(projectDirectory, '.env.production'), + ), + }, + }; + } + + private async locateNuxtConfig(): Promise { + return this.locateFile( + ...['ts', 'js', 'mjs'].map(ext => `nuxt.config.${ext}`), + ); + } + + private getInstallationTasks(installation: Omit): Task[] { + return [ + { + title: 'Register module', + task: async notifier => { + notifier.update('Registering module'); + + try { + await this.applyConfigCodemod(installation.project.config.file); + + notifier.confirm('Module registered'); + } catch (error) { + notifier.alert('Failed to register module', HelpfulError.formatMessage(error)); + } + }, + }, + { + title: 'Setup environment variables', + task: async notifier => { + notifier.update('Setting up environment variables'); + + try { + await this.updateEnvVariables({ + ...installation, + notifier: notifier, + }); + + notifier.confirm('Environment variables updated'); + } catch (error) { + notifier.alert('Failed to update environment variables', HelpfulError.formatMessage(error)); + } + }, + }, + ]; + } + + private async applyConfigCodemod(file: string): Promise { + await this.codemod + .config + .apply(this.fileSystem.joinPaths(this.projectDirectory.get(), file)); + } + + private async updateEnvVariables(installation: NuxtInstallation): Promise { + const {project: {env}, configuration, notifier} = installation; + + notifier.update('Loading information'); + + const [developmentApplication, productionApplication] = await Promise.all([ + this.workspaceApi.getApplication({ + organizationSlug: configuration.organization, + workspaceSlug: configuration.workspace, + applicationSlug: configuration.applications.development, + }), + configuration.applications.production === undefined + ? null + : this.workspaceApi.getApplication({ + organizationSlug: configuration.organization, + workspaceSlug: configuration.workspace, + applicationSlug: configuration.applications.production, + }), + ]); + + if (developmentApplication === null) { + throw new SdkError( + `Development application \`${configuration.applications.development}\` not found.`, + {reason: ErrorReason.NOT_FOUND}, + ); + } + + if (!await env.localFile.hasVariable(NuxtEnvVar.API_KEY) && installation.skipApiKeySetup !== true) { + const user = await this.userApi.getUser(); + + notifier.update('Creating API key'); + + let apiKey: GeneratedApiKey; + + try { + apiKey = await this.applicationApi.createApiKey({ + organizationSlug: configuration.organization, + workspaceSlug: configuration.workspace, + applicationSlug: developmentApplication.slug, + name: `${user.username} CLI`, + permissions: [ApiKeyPermission.ISSUE_TOKEN], + }); + } catch (error) { + if (error instanceof HelpfulError) { + throw new SdkError( + error instanceof ApiError && error.isAccessDenied() + ? 'Your user does not have permission to create an API key' + : error.message, + error.help, + ); + } + + throw error; + } + + await env.localFile.setVariables({ + [NuxtEnvVar.API_KEY]: apiKey.secret, + }); + } + + await Promise.all([ + env.developmentFile.setVariables({ + [NuxtEnvVar.APP_ID]: developmentApplication.publicId, + }), + productionApplication === null + ? Promise.resolve() + : env.productionFile.setVariables({ + [NuxtEnvVar.APP_ID]: productionApplication.publicId, + }), + ]); + } +} diff --git a/src/application/project/sdk/plugVueSdk.ts b/src/application/project/sdk/plugVueSdk.ts new file mode 100644 index 00000000..4933ec36 --- /dev/null +++ b/src/application/project/sdk/plugVueSdk.ts @@ -0,0 +1,295 @@ +import type {Installation} from '@/application/project/sdk/sdk'; +import {SdkError} from '@/application/project/sdk/sdk'; +import type { + InstallationPlan, + Configuration as JavaScriptSdkConfiguration, +} from '@/application/project/sdk/javasScriptSdk'; +import {JavaScriptSdk} from '@/application/project/sdk/javasScriptSdk'; +import type {Codemod} from '@/application/project/code/transformation/codemod'; +import type {Task, TaskNotifier} from '@/application/cli/io/output'; +import type {VuePluginOptions} from '@/application/project/code/transformation/javascript/vuePluginCodemod'; +import {EnvFile} from '@/application/project/code/envFile'; +import type {ExampleFile} from '@/application/project/code/generation/example'; +import {PlugVueExampleGenerator} from '@/application/project/code/generation/slot/plugVueExampleGenerator'; +import type {Slot} from '@/application/model/slot'; +import {ErrorReason, HelpfulError} from '@/application/error'; +import type {ImportResolver} from '@/application/project/import/importResolver'; +import type {AttributeType} from '@/application/project/code/transformation/javascript/utils/createObjectProps'; + +type CodemodConfiguration = { + plugin: Codemod, +}; + +type Bundler = { + package: string, + prefix: string, +}; + +export type Configuration = JavaScriptSdkConfiguration & { + codemod: CodemodConfiguration, + bundlers: Bundler[], + importResolver: ImportResolver, +}; + +type VueProjectInfo = { + typescript: boolean, + sourceDirectory: string, + plugin: { + file: string | null, + }, + env?: { + property: string, + developmentFile: EnvFile, + productionFile: EnvFile, + }, +}; + +type VueInstallation = Installation & { + project: VueProjectInfo, + notifier: TaskNotifier, +}; + +type PublicAppIds = { + development: string, + production?: string, +}; + +export class PlugVueSdk extends JavaScriptSdk { + private readonly codemod: CodemodConfiguration; + + private readonly bundlers: Bundler[]; + + private readonly importResolver: ImportResolver; + + public constructor(configuration: Configuration) { + super(configuration); + + this.codemod = configuration.codemod; + this.bundlers = configuration.bundlers; + this.importResolver = configuration.importResolver; + } + + protected async generateSlotExampleFiles(slot: Slot, installation: Installation): Promise { + const isTypeScript = await this.isTypeScriptProject(); + + const paths = await this.getPaths(installation.configuration); + const slotPath = this.fileSystem.joinPaths(paths.components, '%slug%.vue'); + const pagePath = this.fileSystem.joinPaths(paths.examples, '%slug%-example.vue'); + + const generator = new PlugVueExampleGenerator({ + typescript: isTypeScript, + contentVariable: 'content', + slotImportPath: await this.importResolver.getImportPath( + slotPath, + this.fileSystem.getDirectoryName(pagePath), + ), + slotFilePath: slotPath, + slotComponentName: '%name%', + pageFilePath: pagePath, + }); + + const example = generator.generate({ + id: slot.slug, + version: slot.version.major, + definition: slot.resolvedDefinition, + }); + + return example.files; + } + + protected async getInstallationPlan(installation: Installation): Promise { + const {configuration} = installation; + const projectInfo = await this.getProjectInfo(); + + return { + dependencies: ['@croct/plug-vue'], + tasks: this.getInstallationTasks({ + ...installation, + project: projectInfo, + }), + configuration: configuration, + }; + } + + private async getProjectInfo(): Promise { + const sourceDirectory = 'src'; + const envProperty = await this.getEnvVarProperty(); + const projectDirectory = this.projectDirectory.get(); + + return { + typescript: await this.isTypeScriptProject(), + sourceDirectory: sourceDirectory, + plugin: { + file: await this.locateFile( + ...['main', 'index'].flatMap(name => ['ts', 'js'].map(ext => `${name}.${ext}`)) + .map(file => this.fileSystem.joinPaths(sourceDirectory, file)), + ), + }, + env: envProperty === null + ? undefined + : { + property: envProperty, + productionFile: new EnvFile( + this.fileSystem, + this.fileSystem.joinPaths(projectDirectory, '.env.production'), + ), + developmentFile: new EnvFile( + this.fileSystem, + this.fileSystem.joinPaths(projectDirectory, '.env.development'), + ), + }, + }; + } + + private getInstallationTasks(installation: Omit): Task[] { + const tasks: Task[] = []; + const projectEnv = installation.project.env; + const {configuration} = installation; + + let publicIdsPromise: Promise | null = null; + + const getPublicIds = (): Promise => { + if (publicIdsPromise === null) { + publicIdsPromise = Promise.all([ + this.workspaceApi.getApplication({ + organizationSlug: configuration.organization, + workspaceSlug: configuration.workspace, + applicationSlug: configuration.applications.development, + }), + configuration.applications.production === undefined + ? null + : this.workspaceApi.getApplication({ + organizationSlug: configuration.organization, + workspaceSlug: configuration.workspace, + applicationSlug: configuration.applications.production, + }), + ]).then(([development, production]) => { + if (development === null) { + return Promise.reject( + new SdkError( + `Development application ${configuration.applications.development} not found`, + {reason: ErrorReason.NOT_FOUND}, + ), + ); + } + + return { + development: development.publicId, + production: production?.publicId, + }; + }); + } + + return publicIdsPromise; + }; + + if (projectEnv !== undefined) { + const {developmentFile, productionFile, property} = projectEnv; + const variable = property.split('.').pop()!; + + tasks.push({ + title: 'Setup environment variables', + task: async notifier => { + notifier.update('Setting up environment variables'); + + try { + const publicIds = await getPublicIds(); + + await Promise.all([ + developmentFile.setVariable(variable, publicIds.development), + publicIds.production === undefined + ? Promise.resolve() + : productionFile.setVariable(variable, publicIds.production), + ]); + + notifier.confirm('Environment variables updated'); + } catch (error) { + notifier.alert('Failed to update environment variables', HelpfulError.formatMessage(error)); + } + }, + }); + } + + tasks.push({ + title: 'Register plugin', + task: async notifier => { + notifier.update('Registering plugin'); + + const pluginFile = installation.project.plugin.file; + + try { + if (pluginFile === null) { + notifier.alert('No Vue entry file found'); + } else { + await this.installPlugin(pluginFile, { + args: { + appId: PlugVueSdk.getAppIdProperty(await getPublicIds(), projectEnv?.property), + }, + }); + + notifier.confirm('Plugin registered'); + } + } catch (error) { + notifier.alert('Failed to register plugin', HelpfulError.formatMessage(error)); + } + }, + }); + + return tasks; + } + + private async installPlugin(file: string, options: VuePluginOptions): Promise { + const codemod = this.codemod.plugin; + + await codemod.apply(this.fileSystem.joinPaths(this.projectDirectory.get(), file), options); + } + + private async getEnvVarProperty(): Promise { + for (const bundler of this.bundlers) { + if (await this.packageManager.hasDirectDependency(bundler.package)) { + return `${bundler.prefix}CROCT_APP_ID`; + } + } + + return null; + } + + private static getAppIdProperty(applicationIds: PublicAppIds, env?: string): AttributeType { + if (env !== undefined) { + return { + type: 'reference', + path: env.split('.'), + }; + } + + if (applicationIds.production === undefined) { + return { + type: 'literal', + value: applicationIds.development, + }; + } + + return { + type: 'ternary', + condition: { + operator: '===', + left: { + type: 'reference', + path: ['import', 'meta', 'env', 'MODE'], + }, + right: { + type: 'literal', + value: 'production', + }, + }, + consequent: { + type: 'literal', + value: applicationIds.production, + }, + alternate: { + type: 'literal', + value: applicationIds.development, + }, + }; + } +} From 52851e4f934481bfd3a95d73a1dd37c9cca20828 Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Thu, 28 May 2026 16:34:21 -0300 Subject: [PATCH 6/9] Finish implementation --- src/application/model/platform.ts | 8 + .../slot/plugNuxtExampleGenerator.ts | 5 +- .../javascript/vuePluginCodemod.ts | 37 +++- .../project/sdk/nuxtStoryblokPlugin.ts | 68 ++++++++ src/application/project/sdk/plugNuxtSdk.ts | 95 +++++----- src/application/project/sdk/plugReactSdk.ts | 5 +- src/application/project/sdk/plugVueSdk.ts | 5 +- ...lokPlugin.ts => wrapperStoryblokPlugin.ts} | 54 +++--- .../application/api/graphql/workspace.ts | 7 +- src/infrastructure/application/cli/cli.ts | 138 ++++++++++++++- .../plugNuxtExampleGenerator.test.ts.snap | 164 +++++++++--------- .../slot/plugNuxtExampleGenerator.test.ts | 13 +- .../vue-plugin/chainedWithStoryblok.ts | 10 ++ .../vue-plugin/withStoryblokAppUse.ts | 10 ++ .../vue-storyblok/storyblokBasicMigration.ts | 1 - .../vuePluginCodemod.test.ts.snap | 59 +++++-- .../vueStoryblokCodemod.test.ts.snap | 1 - 17 files changed, 478 insertions(+), 202 deletions(-) create mode 100644 src/application/project/sdk/nuxtStoryblokPlugin.ts rename src/application/project/sdk/{storyblokPlugin.ts => wrapperStoryblokPlugin.ts} (63%) create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/chainedWithStoryblok.ts create mode 100644 test/application/project/code/transformation/fixtures/vue-plugin/withStoryblokAppUse.ts diff --git a/src/application/model/platform.ts b/src/application/model/platform.ts index d4b0b230..67a38c5a 100644 --- a/src/application/model/platform.ts +++ b/src/application/model/platform.ts @@ -1,6 +1,8 @@ export enum Platform { NEXTJS = 'nextjs', + NUXT = 'nuxt', REACT = 'react', + VUE = 'vue', JAVASCRIPT = 'javascript', } @@ -10,9 +12,15 @@ export namespace Platform { case Platform.NEXTJS: return 'Next.js'; + case Platform.NUXT: + return 'Nuxt'; + case Platform.REACT: return 'React'; + case Platform.VUE: + return 'Vue'; + case Platform.JAVASCRIPT: return 'JavaScript'; } diff --git a/src/application/project/code/generation/slot/plugNuxtExampleGenerator.ts b/src/application/project/code/generation/slot/plugNuxtExampleGenerator.ts index c236cf11..708476c2 100644 --- a/src/application/project/code/generation/slot/plugNuxtExampleGenerator.ts +++ b/src/application/project/code/generation/slot/plugNuxtExampleGenerator.ts @@ -4,12 +4,9 @@ import type {CodeWriter} from '@/application/project/code/generation/codeWritter export class PlugNuxtExampleGenerator extends VueExampleGenerator { protected writeSlotScript(writer: CodeWriter, definition: SlotDefinition): void { - const variable = this.options.contentVariable; - const binding = variable === 'data' ? variable : `data: ${variable}`; - this.writeScriptOpening(writer); - writer.write(`const {${binding}} = await useContent('${definition.id}@${definition.version}');`); + writer.write(`const {data} = await useContent('${definition.id}@${definition.version}');`); this.writeScriptClosing(writer); } diff --git a/src/application/project/code/transformation/javascript/vuePluginCodemod.ts b/src/application/project/code/transformation/javascript/vuePluginCodemod.ts index 4b5fdbe3..438657c4 100644 --- a/src/application/project/code/transformation/javascript/vuePluginCodemod.ts +++ b/src/application/project/code/transformation/javascript/vuePluginCodemod.ts @@ -87,7 +87,7 @@ export class VuePluginCodemod implements Codemod { if (anchor.kind === 'chain') { VuePluginCodemod.injectIntoChain(anchor, factoryCall); } else { - VuePluginCodemod.injectBeforeMount(anchor, factoryCall); + VuePluginCodemod.injectAfterDeclaration(anchor, factoryCall); } return Promise.resolve({modified: true, result: input}); @@ -380,17 +380,36 @@ export class VuePluginCodemod implements Codemod { return first !== undefined && t.isIdentifier(first) && first.name === identifier; } + /** + * Inserts the plugin registration as the first link of the chain. + * + * Walking down through the chained calls and splicing the registration right after + * the createApp call ensures the Croct context is set up before any plugin that + * integrates with it, such as the Storyblok bridge. + */ private static injectIntoChain(anchor: ChainAnchor, factoryCall: t.CallExpression): void { - const {mountCall} = anchor; - const receiver = mountCall.callee.object; + let parent: t.MemberExpression = anchor.mountCall.callee; - mountCall.callee.object = t.callExpression( - t.memberExpression(receiver, t.identifier('use')), + // Walk down through chained .use(...) links until we hit the createApp(App) + // call at the bottom. Its callee is an Identifier (createApp), so it fails the + // ChainCall check, which is exactly the stop condition. + while (VuePluginCodemod.isChainCall(parent.object)) { + parent = parent.object.callee; + } + + parent.object = t.callExpression( + t.memberExpression(parent.object, t.identifier('use')), [factoryCall], ); } - private static injectBeforeMount(anchor: VariableAnchor, factoryCall: t.CallExpression): void { + /** + * Inserts the plugin registration immediately after the variable declaration. + * + * Placing the call right next to the declaration guarantees that it runs before any + * other plugin registration against the same binding. + */ + private static injectAfterDeclaration(anchor: VariableAnchor, factoryCall: t.CallExpression): void { const useStatement = t.expressionStatement( t.callExpression( t.memberExpression(t.identifier(anchor.binding), t.identifier('use')), @@ -398,8 +417,10 @@ export class VuePluginCodemod implements Codemod { ), ); - const mountIndex = anchor.parentBody.findIndex(statement => statement === anchor.mountStatement); + const declarationIndex = anchor.parentBody.findIndex( + statement => statement === anchor.declarationStatement, + ); - anchor.parentBody.splice(mountIndex, 0, useStatement); + anchor.parentBody.splice(declarationIndex + 1, 0, useStatement); } } diff --git a/src/application/project/sdk/nuxtStoryblokPlugin.ts b/src/application/project/sdk/nuxtStoryblokPlugin.ts new file mode 100644 index 00000000..5c23f3b4 --- /dev/null +++ b/src/application/project/sdk/nuxtStoryblokPlugin.ts @@ -0,0 +1,68 @@ +import type { + InstallationPlan, + JavaScriptPluginContext, + JavaScriptSdkPlugin, +} from '@/application/project/sdk/javasScriptSdk'; +import type {Task} from '@/application/cli/io/output'; +import {HelpfulError} from '@/application/error'; +import type {Codemod} from '@/application/project/code/transformation/codemod'; +import type {Installation} from '@/application/project/sdk/sdk'; + +export type Configuration = { + storyblokPackage: string, + pluginFile: string, + codemod: Codemod, +}; + +export class NuxtStoryblokPlugin implements JavaScriptSdkPlugin { + private readonly configuration: Configuration; + + public constructor(configuration: Configuration) { + this.configuration = configuration; + } + + public async getInstallationPlan( + _: Installation, + context: JavaScriptPluginContext, + ): Promise> { + if (!await context.packageManager.hasDirectDependency(this.configuration.storyblokPackage)) { + return {}; + } + + const tasks: Task[] = [ + { + title: 'Configure Storyblok integration', + task: async notifier => { + notifier.update('Configuring Storyblok integration'); + + try { + await this.scaffoldPluginFile(context); + + notifier.confirm('Storyblok configured'); + } catch (error) { + notifier.alert('Failed to configure Storyblok', HelpfulError.formatMessage(error)); + } + }, + }, + ]; + + return { + tasks: tasks, + dependencies: ['@croct/plug-storyblok'], + }; + } + + private async scaffoldPluginFile(scope: JavaScriptPluginContext): Promise { + const {fileSystem, projectDirectory} = scope; + const path = fileSystem.joinPaths(projectDirectory.get(), this.configuration.pluginFile); + + if (!await fileSystem.exists(path)) { + await fileSystem.createDirectory(fileSystem.getDirectoryName(path), {recursive: true}); + await fileSystem.writeTextFile(path, '', {overwrite: true}); + } + + await this.configuration + .codemod + .apply(path); + } +} diff --git a/src/application/project/sdk/plugNuxtSdk.ts b/src/application/project/sdk/plugNuxtSdk.ts index 7bfbd5bf..3ad74c5f 100644 --- a/src/application/project/sdk/plugNuxtSdk.ts +++ b/src/application/project/sdk/plugNuxtSdk.ts @@ -17,6 +17,7 @@ import {ApiError} from '@/application/api/error'; import type {Slot} from '@/application/model/slot'; import {ErrorReason, HelpfulError} from '@/application/error'; import {ApiKeyPermission} from '@/application/model/application'; +import type {CommandExecutor} from '@/application/system/process/executor'; type CodemodConfiguration = { config: Codemod, @@ -27,6 +28,7 @@ export type Configuration = JavaScriptSdkConfiguration & { userApi: UserApi, workspaceApi: WorkspaceApi, applicationApi: ApplicationApi, + commandExecutor: CommandExecutor, }; type NuxtProjectInfo = { @@ -34,11 +36,7 @@ type NuxtProjectInfo = { config: { file: string, }, - env: { - localFile: EnvFile, - developmentFile: EnvFile, - productionFile: EnvFile, - }, + envFile: EnvFile, }; type NuxtInstallation = Installation & { @@ -58,12 +56,15 @@ export class PlugNuxtSdk extends JavaScriptSdk { private readonly codemod: CodemodConfiguration; + private readonly commandExecutor: CommandExecutor; + public constructor(configuration: Configuration) { super(configuration); this.codemod = configuration.codemod; this.userApi = configuration.userApi; this.applicationApi = configuration.applicationApi; + this.commandExecutor = configuration.commandExecutor; } protected async generateSlotExampleFiles(slot: Slot, installation: Installation): Promise { @@ -75,7 +76,7 @@ export class PlugNuxtSdk extends JavaScriptSdk { const generator = new PlugNuxtExampleGenerator({ typescript: isTypeScript, - contentVariable: 'content', + contentVariable: 'data.content', slotImportPath: this.fileSystem.joinPaths('~', paths.components, '%slug%.vue'), slotFilePath: slotPath, slotComponentName: '%name%', @@ -125,20 +126,10 @@ export class PlugNuxtSdk extends JavaScriptSdk { config: { file: configFile ?? `nuxt.config.${extension}`, }, - env: { - localFile: new EnvFile( - this.fileSystem, - this.fileSystem.joinPaths(projectDirectory, '.env.local'), - ), - developmentFile: new EnvFile( - this.fileSystem, - this.fileSystem.joinPaths(projectDirectory, '.env.development'), - ), - productionFile: new EnvFile( - this.fileSystem, - this.fileSystem.joinPaths(projectDirectory, '.env.production'), - ), - }, + envFile: new EnvFile( + this.fileSystem, + this.fileSystem.joinPaths(projectDirectory, '.env'), + ), }; } @@ -164,6 +155,20 @@ export class PlugNuxtSdk extends JavaScriptSdk { } }, }, + { + title: 'Generate Nuxt type aliases', + task: async notifier => { + notifier.update('Generating Nuxt type aliases'); + + try { + await this.generateNuxtTypeAliases(); + + notifier.confirm('Nuxt type aliases generated'); + } catch (error) { + notifier.alert('Failed to generate Nuxt type aliases', HelpfulError.formatMessage(error)); + } + }, + }, { title: 'Setup environment variables', task: async notifier => { @@ -190,25 +195,28 @@ export class PlugNuxtSdk extends JavaScriptSdk { .apply(this.fileSystem.joinPaths(this.projectDirectory.get(), file)); } + private async generateNuxtTypeAliases(): Promise { + const command = await this.packageManager.getPackageCommand('nuxi', ['prepare']); + + const execution = await this.commandExecutor.run(command, { + workingDirectory: this.projectDirectory.get(), + }); + + if (await execution.wait() !== 0) { + throw new HelpfulError(`Failed to execute command \`${command.name}\`.`); + } + } + private async updateEnvVariables(installation: NuxtInstallation): Promise { - const {project: {env}, configuration, notifier} = installation; + const {project: {envFile}, configuration, notifier} = installation; notifier.update('Loading information'); - const [developmentApplication, productionApplication] = await Promise.all([ - this.workspaceApi.getApplication({ - organizationSlug: configuration.organization, - workspaceSlug: configuration.workspace, - applicationSlug: configuration.applications.development, - }), - configuration.applications.production === undefined - ? null - : this.workspaceApi.getApplication({ - organizationSlug: configuration.organization, - workspaceSlug: configuration.workspace, - applicationSlug: configuration.applications.production, - }), - ]); + const developmentApplication = await this.workspaceApi.getApplication({ + organizationSlug: configuration.organization, + workspaceSlug: configuration.workspace, + applicationSlug: configuration.applications.development, + }); if (developmentApplication === null) { throw new SdkError( @@ -217,7 +225,7 @@ export class PlugNuxtSdk extends JavaScriptSdk { ); } - if (!await env.localFile.hasVariable(NuxtEnvVar.API_KEY) && installation.skipApiKeySetup !== true) { + if (!await envFile.hasVariable(NuxtEnvVar.API_KEY) && installation.skipApiKeySetup !== true) { const user = await this.userApi.getUser(); notifier.update('Creating API key'); @@ -245,20 +253,13 @@ export class PlugNuxtSdk extends JavaScriptSdk { throw error; } - await env.localFile.setVariables({ + await envFile.setVariables({ [NuxtEnvVar.API_KEY]: apiKey.secret, }); } - await Promise.all([ - env.developmentFile.setVariables({ - [NuxtEnvVar.APP_ID]: developmentApplication.publicId, - }), - productionApplication === null - ? Promise.resolve() - : env.productionFile.setVariables({ - [NuxtEnvVar.APP_ID]: productionApplication.publicId, - }), - ]); + await envFile.setVariables({ + [NuxtEnvVar.APP_ID]: developmentApplication.publicId, + }); } } diff --git a/src/application/project/sdk/plugReactSdk.ts b/src/application/project/sdk/plugReactSdk.ts index 55be544b..c4177d56 100644 --- a/src/application/project/sdk/plugReactSdk.ts +++ b/src/application/project/sdk/plugReactSdk.ts @@ -83,10 +83,7 @@ export class PlugReactSdk extends JavaScriptSdk { ? CodeLanguage.TYPESCRIPT_XML : CodeLanguage.JAVASCRIPT_XML, contentVariable: 'content', - slotImportPath: await this.importResolver.getImportPath( - slotPath, - this.fileSystem.getDirectoryName(pagePath), - ), + slotImportPath: await this.importResolver.getImportPath(slotPath, pagePath), slotFilePath: slotPath, slotComponentName: '%name%', pageFilePath: pagePath, diff --git a/src/application/project/sdk/plugVueSdk.ts b/src/application/project/sdk/plugVueSdk.ts index 4933ec36..64adeb82 100644 --- a/src/application/project/sdk/plugVueSdk.ts +++ b/src/application/project/sdk/plugVueSdk.ts @@ -79,10 +79,7 @@ export class PlugVueSdk extends JavaScriptSdk { const generator = new PlugVueExampleGenerator({ typescript: isTypeScript, contentVariable: 'content', - slotImportPath: await this.importResolver.getImportPath( - slotPath, - this.fileSystem.getDirectoryName(pagePath), - ), + slotImportPath: await this.importResolver.getImportPath(slotPath, pagePath), slotFilePath: slotPath, slotComponentName: '%name%', pageFilePath: pagePath, diff --git a/src/application/project/sdk/storyblokPlugin.ts b/src/application/project/sdk/wrapperStoryblokPlugin.ts similarity index 63% rename from src/application/project/sdk/storyblokPlugin.ts rename to src/application/project/sdk/wrapperStoryblokPlugin.ts index 5e656231..bfc9c5e4 100644 --- a/src/application/project/sdk/storyblokPlugin.ts +++ b/src/application/project/sdk/wrapperStoryblokPlugin.ts @@ -11,44 +11,43 @@ import type {Installation} from '@/application/project/sdk/sdk'; import type {ScanFilter} from '@/application/fs/fileSystem'; export type Configuration = { + storyblokPackage: string, + marker: string, scanFilter: ScanFilter, codemod: Codemod, }; -export class StoryblokPlugin implements JavaScriptSdkPlugin { - private readonly codemod: Codemod; - - private readonly scanFilter: ScanFilter; +export class WrapperStoryblokPlugin implements JavaScriptSdkPlugin { + private readonly configuration: Configuration; public constructor(configuration: Configuration) { - this.codemod = configuration.codemod; - this.scanFilter = configuration.scanFilter; + this.configuration = configuration; } public async getInstallationPlan( _: Installation, context: JavaScriptPluginContext, ): Promise> { - if (!await context.packageManager.hasDependency('@storyblok/js')) { + if (!await context.packageManager.hasDirectDependency(this.configuration.storyblokPackage)) { return {}; } - const tasks: Task[] = []; - - tasks.push({ - title: 'Configure Storyblok integration', - task: async notifier => { - notifier.update('Configuring Storyblok integration'); + const tasks: Task[] = [ + { + title: 'Configure Storyblok integration', + task: async notifier => { + notifier.update('Configuring Storyblok integration'); - try { - await this.configureStoryblok(context); + try { + await this.configureStoryblok(context); - notifier.confirm('Storyblok configured'); - } catch (error) { - notifier.alert('Failed to configure Storyblok', HelpfulError.formatMessage(error)); - } + notifier.confirm('Storyblok configured'); + } catch (error) { + notifier.alert('Failed to configure Storyblok', HelpfulError.formatMessage(error)); + } + }, }, - }); + ]; return { tasks: tasks, @@ -57,14 +56,15 @@ export class StoryblokPlugin implements JavaScriptSdkPlugin { } private async configureStoryblok(scope: JavaScriptPluginContext): Promise { - const initializationFiles = await this.findStoryblokInitializationFiles(scope); + const candidateFiles = await this.findCandidateFiles(scope); - if (initializationFiles.length === 0) { + if (candidateFiles.length === 0) { throw new HelpfulError('Could not find any file containing Storyblok initialization.'); } - const results = initializationFiles.map( - file => this.codemod + const results = candidateFiles.map( + file => this.configuration + .codemod .apply(file) .then(result => result.modified), ); @@ -74,12 +74,12 @@ export class StoryblokPlugin implements JavaScriptSdkPlugin { } } - private async findStoryblokInitializationFiles(scope: JavaScriptPluginContext): Promise { + private async findCandidateFiles(scope: JavaScriptPluginContext): Promise { const {fileSystem, projectDirectory} = scope; const directory = projectDirectory.get(); const iterator = fileSystem.list(directory, async (path, depth) => { - if (!await this.scanFilter(path, depth) || depth > 20) { + if (!await this.configuration.scanFilter(path, depth) || depth > 20) { return false; } @@ -103,7 +103,7 @@ export class StoryblokPlugin implements JavaScriptSdkPlugin { const path = fileSystem.joinPaths(directory, entry.name); const content = await fileSystem.readTextFile(path); - if (content.includes('storyblokInit')) { + if (content.includes(this.configuration.marker)) { files.push(path); } } diff --git a/src/infrastructure/application/api/graphql/workspace.ts b/src/infrastructure/application/api/graphql/workspace.ts index fe8c4103..24b51c0f 100644 --- a/src/infrastructure/application/api/graphql/workspace.ts +++ b/src/infrastructure/application/api/graphql/workspace.ts @@ -121,10 +121,15 @@ function createNormalizationMap(map: Record< }; } +// The GraphQL schema does not yet have first-class Vue/Nuxt platforms, so they are +// reported to the server as JavaScript. JAVASCRIPT is intentionally listed last so that +// the inverse (api -> model) map resolves the shared `Javascript` value back to JAVASCRIPT. const platformMap = createNormalizationMap({ - [Platform.JAVASCRIPT]: GraphqlPlatform.Javascript, + [Platform.VUE]: GraphqlPlatform.Javascript, + [Platform.NUXT]: GraphqlPlatform.Javascript, [Platform.REACT]: GraphqlPlatform.React, [Platform.NEXTJS]: GraphqlPlatform.Next, + [Platform.JAVASCRIPT]: GraphqlPlatform.Javascript, }); const environmentMap = createNormalizationMap({ diff --git a/src/infrastructure/application/cli/cli.ts b/src/infrastructure/application/cli/cli.ts index 7ffbd716..24329ff7 100644 --- a/src/infrastructure/application/cli/cli.ts +++ b/src/infrastructure/application/cli/cli.ts @@ -22,6 +22,8 @@ import type { import {PlugJsSdk} from '@/application/project/sdk/plugJsSdk'; import {PlugReactSdk} from '@/application/project/sdk/plugReactSdk'; import {PlugNextSdk} from '@/application/project/sdk/plugNextSdk'; +import {PlugVueSdk} from '@/application/project/sdk/plugVueSdk'; +import {PlugNuxtSdk} from '@/application/project/sdk/plugNuxtSdk'; import type {InitInput} from '@/application/cli/command/init'; import {InitCommand} from '@/application/cli/command/init'; import type {LoginInput} from '@/application/cli/command/login'; @@ -349,7 +351,15 @@ import { CreateApiKeyOptionsValidator, } from '@/infrastructure/application/validation/actions/createApiKeyOptionsValidator'; import {StoryblokInitCodemod} from '@/application/project/code/transformation/javascript/storyblokInitCodemod'; -import {StoryblokPlugin} from '@/application/project/sdk/storyblokPlugin'; +import {WrapperStoryblokPlugin} from '@/application/project/sdk/wrapperStoryblokPlugin'; +import {NuxtStoryblokPlugin} from '@/application/project/sdk/nuxtStoryblokPlugin'; +import {VuePluginCodemod} from '@/application/project/code/transformation/javascript/vuePluginCodemod'; +import {VueStoryblokCodemod} from '@/application/project/code/transformation/javascript/vueStoryblokCodemod'; +import {NuxtConfigModuleCodemod} from '@/application/project/code/transformation/javascript/nuxtConfigModuleCodemod'; + +import { + NuxtStoryblokPluginCodemod, +} from '@/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod'; export type Configuration = { program: Program, @@ -1801,6 +1811,59 @@ export class Cli { }, ], }), + [Platform.VUE]: (): Sdk => new PlugVueSdk({ + ...config, + plugins: [this.createVueStoryblokPlugin()], + importResolver: importResolver, + codemod: { + plugin: new FormatCodemod( + formatter, + new FileCodemod({ + fileSystem: this.getFileSystem(), + codemod: new JavaScriptCodemod({ + languages: ['typescript'], + codemod: new VuePluginCodemod({ + plugin: { + module: '@croct/plug-vue', + factory: 'createCroct', + }, + }), + }), + }), + ), + }, + bundlers: [ + { + package: 'vite', + prefix: 'import.meta.env.VITE_', + }, + { + package: 'parcel', + prefix: 'process.env.', + }, + ], + }), + [Platform.NUXT]: (): Sdk => new PlugNuxtSdk({ + ...config, + plugins: [this.createNuxtStoryblokPlugin()], + userApi: this.getUserApi(), + applicationApi: this.getApplicationApi(), + commandExecutor: this.getAsynchronousCommandExecutor(), + codemod: { + config: new FormatCodemod( + formatter, + new FileCodemod({ + fileSystem: this.getFileSystem(), + codemod: new JavaScriptCodemod({ + languages: ['typescript'], + codemod: new NuxtConfigModuleCodemod({ + moduleName: '@croct/plug-nuxt', + }), + }), + }), + ), + }, + }), [Platform.NEXTJS]: (): Sdk => { const providerProps: Record = { appId: { @@ -1947,6 +2010,8 @@ export class Cli { [Platform.JAVASCRIPT]: () => this.getJavaScriptFormatter(), [Platform.REACT]: () => this.getJavaScriptFormatter(), [Platform.NEXTJS]: () => this.getJavaScriptFormatter(), + [Platform.VUE]: () => this.getJavaScriptFormatter(), + [Platform.NUXT]: () => this.getJavaScriptFormatter(), [unknown]: (): never => { throw new ProviderError('No code formatter detected.', { reason: ErrorReason.NOT_SUPPORTED, @@ -1971,7 +2036,9 @@ export class Cli { [Platform.NEXTJS]: 'next', }; - return new StoryblokPlugin({ + return new WrapperStoryblokPlugin({ + storyblokPackage: '@storyblok/js', + marker: 'storyblokInit', scanFilter: this.getScanFilter(), codemod: new FormatCodemod( this.getJavaScriptFormatter(), @@ -1991,6 +2058,57 @@ export class Cli { }); } + private createVueStoryblokPlugin(): JavaScriptSdkPlugin { + return new WrapperStoryblokPlugin({ + storyblokPackage: '@storyblok/vue', + marker: 'StoryblokVue', + scanFilter: this.getScanFilter(), + codemod: new FormatCodemod( + this.getJavaScriptFormatter(), + new FileCodemod({ + fileSystem: this.getFileSystem(), + codemod: new JavaScriptCodemod({ + languages: ['typescript'], + codemod: new VueStoryblokCodemod({ + plugin: { + module: '@croct/plug-storyblok/vue', + factory: 'withCroct', + }, + storyblok: { + module: '@storyblok/vue', + identifier: 'StoryblokVue', + }, + }), + }), + }), + ), + }); + } + + private createNuxtStoryblokPlugin(): JavaScriptSdkPlugin { + return new NuxtStoryblokPlugin({ + storyblokPackage: '@storyblok/nuxt', + pluginFile: 'plugins/croct-storyblok.ts', + codemod: new FormatCodemod( + this.getJavaScriptFormatter(), + new FileCodemod({ + fileSystem: this.getFileSystem(), + codemod: new JavaScriptCodemod({ + languages: ['typescript'], + codemod: new NuxtStoryblokPluginCodemod({ + plugin: { + module: '@croct/plug-storyblok/nuxt', + factory: 'withCroct', + }, + storyblokVueModule: '@storyblok/vue', + nuxtAppModule: '#app', + }), + }), + }), + ), + }); + } + private share any)>(method: M, factory: () => ReturnType): ReturnType { const instance = this.instances.get(method); @@ -2165,6 +2283,8 @@ export class Cli { [Platform.JAVASCRIPT]: () => this.getNodeServerProvider().get(), [Platform.REACT]: () => this.getNodeServerProvider().get(), [Platform.NEXTJS]: () => this.getNodeServerProvider().get(), + [Platform.VUE]: () => this.getNodeServerProvider().get(), + [Platform.NUXT]: () => this.getNodeServerProvider().get(), [unknown]: () => null, }, }); @@ -2313,6 +2433,13 @@ export class Cli { dependencies: ['next'], }), }, + { + value: Platform.NUXT, + condition: new HasDependency({ + packageManager: nodePackageManager, + dependencies: ['nuxt'], + }), + }, { value: Platform.REACT, condition: new HasDependency({ @@ -2320,6 +2447,13 @@ export class Cli { dependencies: ['react'], }), }, + { + value: Platform.VUE, + condition: new HasDependency({ + packageManager: nodePackageManager, + dependencies: ['vue'], + }), + }, { value: Platform.JAVASCRIPT, condition: new IsProject({ diff --git a/test/application/project/code/generation/slot/__snapshots__/plugNuxtExampleGenerator.test.ts.snap b/test/application/project/code/generation/slot/__snapshots__/plugNuxtExampleGenerator.test.ts.snap index f9ea798c..a5bf997c 100644 --- a/test/application/project/code/generation/slot/__snapshots__/plugNuxtExampleGenerator.test.ts.snap +++ b/test/application/project/code/generation/slot/__snapshots__/plugNuxtExampleGenerator.test.ts.snap @@ -12,13 +12,13 @@ exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for booleanWit }, { "code": " ", "language": "vue", @@ -40,12 +40,12 @@ exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for labeledAtt }, { "code": " ", "language": "vue", @@ -67,15 +67,15 @@ exports[`PlugNuxtExampleGenerator should generate JavaScript SFCs for listOfScal }, { "code": "