From 9cbea0fa5b3f3d7bd7cd2b6940ba48a8e1df6c16 Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Fri, 11 Sep 2026 17:11:44 -0300 Subject: [PATCH] Support the Nuxt 4 app directory and custom Storyblok plugins --- .../javascript/nuxtConfigModuleCodemod.ts | 8 +- .../javascript/nuxtConfigParser.ts | 103 ++++++++++++++++ .../javascript/nuxtStoryblokPluginCodemod.ts | 37 +++++- src/application/project/sdk/javasScriptSdk.ts | 8 +- .../project/sdk/nuxtStoryblokPlugin.ts | 4 +- src/application/project/sdk/plugNuxtSdk.ts | 104 ++++++++++++++-- src/infrastructure/application/cli/cli.ts | 3 + .../fixtures/nuxt-config/aliasedDefineCall.ts | 8 ++ .../fixtures/nuxt-config/bareObjectExport.ts | 6 + .../fixtures/nuxt-config/commonJsExport.js | 6 + .../nuxt-config/compatibilityVersionOnly.ts | 5 + .../nuxt-config/constantReferences.ts | 9 ++ .../fixtures/nuxt-config/defineCall.ts | 6 + .../nuxt-config/defineCallWithReference.ts | 8 ++ .../nuxt-config/duplicateProperties.ts | 8 ++ .../fixtures/nuxt-config/empty.ts | 0 .../fixtures/nuxt-config/ignoredProperties.ts | 13 ++ .../nuxt-config/indirectConfigVariable.ts | 8 ++ .../fixtures/nuxt-config/invalidCode.js | 2 + .../nuxt-config/invalidOptionTypes.ts | 6 + .../fixtures/nuxt-config/nestedOptions.ts | 5 + .../fixtures/nuxt-config/noLayoutOptions.ts | 3 + .../fixtures/nuxt-config/nonLiteralValues.ts | 8 ++ .../fixtures/nuxt-config/nonObjectFuture.ts | 6 + .../fixtures/nuxt-config/srcDirOnly.ts | 3 + .../fixtures/nuxt-config/stringKeys.ts | 6 + .../fixtures/nuxt-config/templateLiteral.ts | 6 + .../fixtures/nuxt-config/withTypeAssertion.ts | 8 ++ .../alreadyScaffoldedObjectSyntax.ts | 11 ++ .../defaultExportSpecifier.ts | 3 + .../existingDefaultExport.ts | 3 + .../nuxt-storyblok-plugin/importsOnly.ts | 2 + .../nuxt-storyblok-plugin/namedExport.ts | 3 + .../namedPluginExport.ts | 3 + .../stringDefaultExportSpecifier.ts | 3 + .../nuxt-storyblok-plugin/unrelatedCalls.ts | 2 + .../nuxtStoryblokPluginCodemod.test.ts.snap | 113 +++++++++++++++++- .../javascript/nuxtConfigParser.test.ts | 48 ++++++++ .../nuxtStoryblokPluginCodemod.test.ts | 1 + 39 files changed, 565 insertions(+), 24 deletions(-) create mode 100644 src/application/project/code/transformation/javascript/nuxtConfigParser.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/aliasedDefineCall.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/bareObjectExport.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/commonJsExport.js create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/compatibilityVersionOnly.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/constantReferences.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/defineCall.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/defineCallWithReference.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/duplicateProperties.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/empty.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/ignoredProperties.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/indirectConfigVariable.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/invalidCode.js create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/invalidOptionTypes.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/nestedOptions.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/noLayoutOptions.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/nonLiteralValues.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/nonObjectFuture.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/srcDirOnly.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/stringKeys.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/templateLiteral.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-config/withTypeAssertion.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/alreadyScaffoldedObjectSyntax.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/defaultExportSpecifier.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/existingDefaultExport.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/importsOnly.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/namedExport.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/namedPluginExport.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/stringDefaultExportSpecifier.ts create mode 100644 test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/unrelatedCalls.ts create mode 100644 test/application/project/code/transformation/javascript/nuxtConfigParser.test.ts diff --git a/src/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.ts b/src/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.ts index 636046db..32f2fc97 100644 --- a/src/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.ts +++ b/src/application/project/code/transformation/javascript/nuxtConfigModuleCodemod.ts @@ -94,7 +94,13 @@ export class NuxtConfigModuleCodemod implements Codemod return false; } - private static findConfig(ast: t.File): t.ObjectExpression | null { + /** + * Locates the object literal holding the Nuxt configuration. + * + * @param ast The parsed configuration file. + * @returns The configuration object, or null if it cannot be statically resolved. + */ + public static findConfig(ast: t.File): t.ObjectExpression | null { const defineName = NuxtConfigModuleCodemod.resolveDefineName(ast); let configObject: t.ObjectExpression | null = null; diff --git a/src/application/project/code/transformation/javascript/nuxtConfigParser.ts b/src/application/project/code/transformation/javascript/nuxtConfigParser.ts new file mode 100644 index 00000000..fe6d9f13 --- /dev/null +++ b/src/application/project/code/transformation/javascript/nuxtConfigParser.ts @@ -0,0 +1,103 @@ +import * as t from '@babel/types'; +import type {NodePath} from '@babel/core'; +import {traverse} from '@babel/core'; +import {parse} from '@/application/project/code/transformation/javascript/utils/parse'; +import {NuxtConfigModuleCodemod} from '@/application/project/code/transformation/javascript/nuxtConfigModuleCodemod'; + +export type NuxtConfig = { + srcDir?: string, + future?: { + compatibilityVersion?: number, + }, +}; + +/** + * Parses the Nuxt configuration options that determine the project layout. + * + * The extraction is static and best-effort: an option is only reported when + * its value can be evaluated without running the configuration, such as + * literals and references to constants. + */ +export class NuxtConfigParser { + public parse(source: string): NuxtConfig { + let ast: t.File; + + try { + ast = parse(source, ['typescript']); + } catch { + return {}; + } + + const config = NuxtConfigModuleCodemod.findConfig(ast); + + if (config === null) { + return {}; + } + + const result: NuxtConfig = {}; + + traverse(ast, { + ObjectExpression: path => { + if (path.node !== config) { + return; + } + + const srcDir = NuxtConfigParser.evaluateProperty(path, 'srcDir'); + + if (typeof srcDir === 'string') { + result.srcDir = srcDir; + } + + const future = NuxtConfigParser.findProperty(path, 'future')?.get('value'); + + if (future !== undefined && future.isObjectExpression()) { + const version = NuxtConfigParser.evaluateProperty(future, 'compatibilityVersion'); + + if (typeof version === 'number') { + result.future = { + compatibilityVersion: version, + }; + } + } + + path.stop(); + }, + }); + + return result; + } + + private static evaluateProperty(object: NodePath, name: string): unknown { + const evaluation = NuxtConfigParser.findProperty(object, name) + ?.get('value') + .evaluate(); + + return evaluation?.confident === true ? evaluation.value : undefined; + } + + private static findProperty( + object: NodePath, + name: string, + ): NodePath | null { + let match: NodePath | null = null; + + // Later properties override earlier ones, as in the evaluated object + for (const property of object.get('properties')) { + if ( + property.isObjectProperty() + && !property.node.computed + && NuxtConfigParser.hasKey(property.node, name) + ) { + match = property; + } + } + + return match; + } + + private static hasKey(property: t.ObjectProperty, name: string): boolean { + const {key} = property; + + return (t.isIdentifier(key) && key.name === name) || (t.isStringLiteral(key) && key.value === name); + } +} diff --git a/src/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.ts b/src/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.ts index 4902a451..20a69b84 100644 --- a/src/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.ts +++ b/src/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.ts @@ -9,6 +9,7 @@ export type NuxtStoryblokPluginConfiguration = { module: string, factory: string, }, + pluginName: string, storyblokVueModule: string, nuxtAppModule: string, }; @@ -16,9 +17,12 @@ export type NuxtStoryblokPluginConfiguration = { /** * 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. + * Generates the canonical plugin that wires the Storyblok API into Croct. + * The plugin runs after the regular ones because the Storyblok API may be + * installed by an application plugin (e.g., `storyblok.ts`) rather than by + * the Storyblok module, and Nuxt runs application plugins in filename order. + * Leaves hand-edited files untouched: if the file already has a default + * export or a plugin definition, the codemod returns unmodified. */ export class NuxtStoryblokPluginCodemod implements Codemod { private readonly configuration: NuxtStoryblokPluginConfiguration; @@ -50,7 +54,9 @@ export class NuxtStoryblokPluginCodemod implements Codemod { + found = true; + + path.stop(); + }, + ExportSpecifier: path => { + const {exported} = path.node; + + if ((t.isIdentifier(exported) ? exported.name : exported.value) === 'default') { + found = true; + + path.stop(); + } + }, CallExpression: path => { const {callee} = path.node; diff --git a/src/application/project/sdk/javasScriptSdk.ts b/src/application/project/sdk/javasScriptSdk.ts index 1f802718..f0c7eaf8 100644 --- a/src/application/project/sdk/javasScriptSdk.ts +++ b/src/application/project/sdk/javasScriptSdk.ts @@ -47,6 +47,7 @@ export type JavaScriptPluginContext = { packageManager: PackageManager, projectDirectory: WorkingDirectory, fileSystem: FileSystem, + paths: ProjectPaths, }; export type JavaScriptSdkPlugin = { @@ -318,14 +319,17 @@ export abstract class JavaScriptSdk implements Sdk { return defaultPath; } - private resolveInstallationPlan(installation: Installation): Promise { - let promise = this.getInstallationPlan(installation); + private async resolveInstallationPlan(installation: Installation): Promise { + const sdkPlan = await this.getInstallationPlan(installation); const context: JavaScriptPluginContext = { packageManager: this.packageManager, projectDirectory: this.projectDirectory, fileSystem: this.fileSystem, + paths: await this.getPaths(sdkPlan.configuration), }; + let promise = Promise.resolve(sdkPlan); + for (const plugin of this.plugins) { promise = promise.then(async plan => { const hookPlan = await plugin.getInstallationPlan(installation, context); diff --git a/src/application/project/sdk/nuxtStoryblokPlugin.ts b/src/application/project/sdk/nuxtStoryblokPlugin.ts index 193bf80b..4faad0fa 100644 --- a/src/application/project/sdk/nuxtStoryblokPlugin.ts +++ b/src/application/project/sdk/nuxtStoryblokPlugin.ts @@ -49,8 +49,8 @@ export class NuxtStoryblokPlugin implements JavaScriptSdkPlugin { } private async scaffoldPluginFile(scope: JavaScriptPluginContext): Promise { - const {fileSystem, projectDirectory} = scope; - const path = fileSystem.joinPaths(projectDirectory.get(), this.configuration.pluginFile); + const {fileSystem, projectDirectory, paths} = scope; + const path = fileSystem.joinPaths(projectDirectory.get(), paths.source, this.configuration.pluginFile); if (!await fileSystem.exists(path)) { await fileSystem.createDirectory(fileSystem.getDirectoryName(path), {recursive: true}); diff --git a/src/application/project/sdk/plugNuxtSdk.ts b/src/application/project/sdk/plugNuxtSdk.ts index 4e109e6d..e1921926 100644 --- a/src/application/project/sdk/plugNuxtSdk.ts +++ b/src/application/project/sdk/plugNuxtSdk.ts @@ -17,6 +17,11 @@ import type {Example} from '@/application/project/example/example'; import {UrlExample} from '@/application/project/example/example'; import {ApiKeyPermission} from '@/application/model/application'; import type {CommandExecutor} from '@/application/system/process/executor'; +import type {ProjectConfiguration, ProjectPaths} from '@/application/project/configuration/projectConfiguration'; +import type { + NuxtConfig, + NuxtConfigParser, +} from '@/application/project/code/transformation/javascript/nuxtConfigParser'; type CodemodConfiguration = { config: Codemod, @@ -24,6 +29,7 @@ type CodemodConfiguration = { export type Configuration = JavaScriptSdkConfiguration & { codemod: CodemodConfiguration, + configParser: NuxtConfigParser, userApi: UserApi, workspaceApi: WorkspaceApi, applicationApi: ApplicationApi, @@ -49,23 +55,42 @@ enum NuxtEnvVar { } export class PlugNuxtSdk extends JavaScriptSdk { + private static readonly CONFIG_FILES = ['ts', 'js', 'mjs'].map(extension => `nuxt.config.${extension}`); + private readonly userApi: UserApi; private readonly applicationApi: ApplicationApi; private readonly codemod: CodemodConfiguration; + private readonly configParser: NuxtConfigParser; + private readonly commandExecutor: CommandExecutor; public constructor(configuration: Configuration) { super(configuration); this.codemod = configuration.codemod; + this.configParser = configuration.configParser; this.userApi = configuration.userApi; this.applicationApi = configuration.applicationApi; this.commandExecutor = configuration.commandExecutor; } + public async getPaths(configuration: ProjectConfiguration): Promise { + const source = configuration.paths?.source ?? await this.getSourceDirectory(); + + return super.getPaths({ + ...configuration, + paths: { + ...configuration.paths, + source: source, + // Examples are pages, which Nuxt routes automatically + examples: configuration.paths?.examples ?? this.fileSystem.joinPaths(source, 'pages'), + }, + }); + } + protected createExample(slot: Slot): Promise { // Nuxt auto-routes `pages//index.vue` to `/`. return Promise.resolve(new UrlExample(slot.name, `/${slot.slug}`)); @@ -81,7 +106,12 @@ export class PlugNuxtSdk extends JavaScriptSdk { const generator = new PlugNuxtExampleGenerator({ typescript: isTypeScript, contentVariable: 'data.content', - slotImportPath: this.fileSystem.joinPaths('~', paths.components, '%slug%.vue'), + // Nuxt resolves `~` to the source directory + slotImportPath: this.fileSystem.joinPaths( + '~', + this.fileSystem.getRelativePath(paths.source, paths.components), + '%slug%.vue', + ), slotFilePath: slotPath, slotComponentName: '%name%', pageFilePath: pagePath, @@ -106,13 +136,7 @@ export class PlugNuxtSdk extends JavaScriptSdk { ...installation, project: projectInfo, }), - configuration: { - ...configuration, - paths: { - ...configuration.paths, - examples: 'pages', - }, - }, + configuration: configuration, }; } @@ -138,9 +162,69 @@ export class PlugNuxtSdk extends JavaScriptSdk { } private async locateNuxtConfig(): Promise { - return this.locateFile( - ...['ts', 'js', 'mjs'].map(ext => `nuxt.config.${ext}`), + return this.locateFile(...PlugNuxtSdk.CONFIG_FILES); + } + + private async getConfig(): Promise { + const source = await this.readFile(...PlugNuxtSdk.CONFIG_FILES).catch(() => null); + + return source === null ? {} : this.configParser.parse(source); + } + + /** + * Resolves the source directory following the rules Nuxt applies to `srcDir`. + * + * Unless configured otherwise, Nuxt 4, and Nuxt 3 opted into the version 4 + * behavior, use `app/` as the source directory, falling back to the root + * directory for projects that still follow the previous layout. + */ + private async getSourceDirectory(): Promise { + const [config, isNuxt4] = await Promise.all([ + this.getConfig(), + // Includes the 4.0 pre-releases + this.packageManager.hasDirectDependency('nuxt', '>=4.0.0-0'), + ]); + + if (config.srcDir !== undefined) { + const root = this.projectDirectory.get(); + const directory = this.fileSystem.getRelativePath(root, this.fileSystem.joinPaths(root, config.srcDir)); + + return directory === '' ? '.' : directory; + } + + if (!isNuxt4 && config.future?.compatibilityVersion !== 4) { + return '.'; + } + + return await this.usesAppDirectory() ? 'app' : '.'; + } + + private async usesAppDirectory(): Promise { + const directory = this.fileSystem.joinPaths(this.projectDirectory.get(), 'app'); + + if (!await this.fileSystem.isDirectory(directory)) { + return false; + } + + for await (const entry of this.fileSystem.list(directory, (_, depth) => depth === 0)) { + // Nuxt 3 already kept these files in `app/`, so they do not indicate the new layout + if (entry.name !== 'spa-loading-template.html' && !entry.name.startsWith('router.options')) { + return true; + } + } + + // Otherwise, `app/` is the source directory unless the root follows the previous layout + const rootLayoutEntry = await this.locateFile( + 'app.vue', + 'App.vue', + 'assets', + 'layouts', + 'middleware', + 'pages', + 'plugins', ); + + return rootLayoutEntry === null; } private getInstallationTasks(installation: Omit): Task[] { diff --git a/src/infrastructure/application/cli/cli.ts b/src/infrastructure/application/cli/cli.ts index 2696acde..bcbee0d2 100644 --- a/src/infrastructure/application/cli/cli.ts +++ b/src/infrastructure/application/cli/cli.ts @@ -379,6 +379,7 @@ 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 {NuxtConfigParser} from '@/application/project/code/transformation/javascript/nuxtConfigParser'; import {ViteConfigPluginCodemod} from '@/application/project/code/transformation/javascript/viteConfigPluginCodemod'; import { HydrogenMiddlewareCodemod, @@ -1901,6 +1902,7 @@ export class Cli { [Platform.NUXT]: (): Sdk => new PlugNuxtSdk({ ...config, plugins: [this.createNuxtStoryblokPlugin()], + configParser: new NuxtConfigParser(), userApi: this.getUserApi(), applicationApi: this.getApplicationApi(), commandExecutor: this.getAsynchronousCommandExecutor(), @@ -2303,6 +2305,7 @@ export class Cli { module: '@croct/plug-storyblok/nuxt', factory: 'withCroct', }, + pluginName: 'croct-storyblok', storyblokVueModule: '@storyblok/vue', nuxtAppModule: '#app', }), diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/aliasedDefineCall.ts b/test/application/project/code/transformation/fixtures/nuxt-config/aliasedDefineCall.ts new file mode 100644 index 00000000..657b5027 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/aliasedDefineCall.ts @@ -0,0 +1,8 @@ +import {defineNuxtConfig as defineConfig} from 'nuxt/config'; + +export default defineConfig({ + srcDir: 'src', + future: { + compatibilityVersion: 4, + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/bareObjectExport.ts b/test/application/project/code/transformation/fixtures/nuxt-config/bareObjectExport.ts new file mode 100644 index 00000000..7a4be9a6 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/bareObjectExport.ts @@ -0,0 +1,6 @@ +export default { + srcDir: 'src', + future: { + compatibilityVersion: 4, + }, +}; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/commonJsExport.js b/test/application/project/code/transformation/fixtures/nuxt-config/commonJsExport.js new file mode 100644 index 00000000..cad31b04 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/commonJsExport.js @@ -0,0 +1,6 @@ +module.exports = { + srcDir: 'src', + future: { + compatibilityVersion: 4, + }, +}; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/compatibilityVersionOnly.ts b/test/application/project/code/transformation/fixtures/nuxt-config/compatibilityVersionOnly.ts new file mode 100644 index 00000000..c3dec556 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/compatibilityVersionOnly.ts @@ -0,0 +1,5 @@ +export default defineNuxtConfig({ + future: { + compatibilityVersion: 4, + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/constantReferences.ts b/test/application/project/code/transformation/fixtures/nuxt-config/constantReferences.ts new file mode 100644 index 00000000..060bd812 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/constantReferences.ts @@ -0,0 +1,9 @@ +const sourceDirectory = 'src'; +const version = 4; + +export default defineNuxtConfig({ + srcDir: sourceDirectory, + future: { + compatibilityVersion: version, + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/defineCall.ts b/test/application/project/code/transformation/fixtures/nuxt-config/defineCall.ts new file mode 100644 index 00000000..51e01265 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/defineCall.ts @@ -0,0 +1,6 @@ +export default defineNuxtConfig({ + srcDir: 'src', + future: { + compatibilityVersion: 4, + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/defineCallWithReference.ts b/test/application/project/code/transformation/fixtures/nuxt-config/defineCallWithReference.ts new file mode 100644 index 00000000..1207d37b --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/defineCallWithReference.ts @@ -0,0 +1,8 @@ +const options = { + srcDir: 'src', + future: { + compatibilityVersion: 4, + }, +}; + +export default defineNuxtConfig(options); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/duplicateProperties.ts b/test/application/project/code/transformation/fixtures/nuxt-config/duplicateProperties.ts new file mode 100644 index 00000000..11cba12e --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/duplicateProperties.ts @@ -0,0 +1,8 @@ +export default defineNuxtConfig({ + srcDir: 'app', + future: { + compatibilityVersion: 3, + compatibilityVersion: 4, + }, + srcDir: 'src', +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/empty.ts b/test/application/project/code/transformation/fixtures/nuxt-config/empty.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/ignoredProperties.ts b/test/application/project/code/transformation/fixtures/nuxt-config/ignoredProperties.ts new file mode 100644 index 00000000..0bdccefa --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/ignoredProperties.ts @@ -0,0 +1,13 @@ +const base = {srcDir: 'base'}; + +export default defineNuxtConfig({ + ...base, + ['srcDir']: 'computed', + 1: 'numeric', + hooks() {}, + srcDir: 'src', + future: { + ...base, + compatibilityVersion: 4, + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/indirectConfigVariable.ts b/test/application/project/code/transformation/fixtures/nuxt-config/indirectConfigVariable.ts new file mode 100644 index 00000000..8f333199 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/indirectConfigVariable.ts @@ -0,0 +1,8 @@ +const config = defineNuxtConfig({ + srcDir: 'src', + future: { + compatibilityVersion: 4, + }, +}); + +export default config; diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/invalidCode.js b/test/application/project/code/transformation/fixtures/nuxt-config/invalidCode.js new file mode 100644 index 00000000..ce92cb21 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/invalidCode.js @@ -0,0 +1,2 @@ +export default defineNuxtConfig({ + srcDir: 'src', diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/invalidOptionTypes.ts b/test/application/project/code/transformation/fixtures/nuxt-config/invalidOptionTypes.ts new file mode 100644 index 00000000..d691e0e0 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/invalidOptionTypes.ts @@ -0,0 +1,6 @@ +export default defineNuxtConfig({ + srcDir: false, + future: { + compatibilityVersion: '4', + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/nestedOptions.ts b/test/application/project/code/transformation/fixtures/nuxt-config/nestedOptions.ts new file mode 100644 index 00000000..ddb058b1 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/nestedOptions.ts @@ -0,0 +1,5 @@ +export default defineNuxtConfig({ + modules: [ + ['some-module', {srcDir: 'src', future: {compatibilityVersion: 4}}], + ], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/noLayoutOptions.ts b/test/application/project/code/transformation/fixtures/nuxt-config/noLayoutOptions.ts new file mode 100644 index 00000000..d3105a41 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/noLayoutOptions.ts @@ -0,0 +1,3 @@ +export default defineNuxtConfig({ + modules: ['@croct/plug-nuxt'], +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/nonLiteralValues.ts b/test/application/project/code/transformation/fixtures/nuxt-config/nonLiteralValues.ts new file mode 100644 index 00000000..cbac113d --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/nonLiteralValues.ts @@ -0,0 +1,8 @@ +import {fileURLToPath} from 'node:url'; + +export default defineNuxtConfig({ + srcDir: fileURLToPath(new URL('./src', import.meta.url)), + future: { + compatibilityVersion: Number(process.env.NUXT_COMPATIBILITY_VERSION), + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/nonObjectFuture.ts b/test/application/project/code/transformation/fixtures/nuxt-config/nonObjectFuture.ts new file mode 100644 index 00000000..3bf7ff1f --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/nonObjectFuture.ts @@ -0,0 +1,6 @@ +import {future} from './shared'; + +export default defineNuxtConfig({ + srcDir: 'src', + future: future, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/srcDirOnly.ts b/test/application/project/code/transformation/fixtures/nuxt-config/srcDirOnly.ts new file mode 100644 index 00000000..e5e5435b --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/srcDirOnly.ts @@ -0,0 +1,3 @@ +export default defineNuxtConfig({ + srcDir: 'src', +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/stringKeys.ts b/test/application/project/code/transformation/fixtures/nuxt-config/stringKeys.ts new file mode 100644 index 00000000..14d7ea2f --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/stringKeys.ts @@ -0,0 +1,6 @@ +export default defineNuxtConfig({ + 'srcDir': 'src', + 'future': { + 'compatibilityVersion': 4, + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/templateLiteral.ts b/test/application/project/code/transformation/fixtures/nuxt-config/templateLiteral.ts new file mode 100644 index 00000000..b866ede3 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/templateLiteral.ts @@ -0,0 +1,6 @@ +export default defineNuxtConfig({ + srcDir: `src`, + future: { + compatibilityVersion: 4, + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-config/withTypeAssertion.ts b/test/application/project/code/transformation/fixtures/nuxt-config/withTypeAssertion.ts new file mode 100644 index 00000000..891ad353 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-config/withTypeAssertion.ts @@ -0,0 +1,8 @@ +import type {NuxtConfig} from 'nuxt/schema'; + +export default { + srcDir: 'src', + future: { + compatibilityVersion: 4, + }, +} satisfies NuxtConfig; diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/alreadyScaffoldedObjectSyntax.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/alreadyScaffoldedObjectSyntax.ts new file mode 100644 index 00000000..2a5a1f96 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/alreadyScaffoldedObjectSyntax.ts @@ -0,0 +1,11 @@ +import { withCroct } from '@croct/plug-storyblok/nuxt'; +import { useStoryblokApi } from '@storyblok/vue'; +import { defineNuxtPlugin } from '#app'; + +export default defineNuxtPlugin({ + name: 'croct-storyblok', + enforce: 'post', + setup(nuxtApp) { + withCroct(nuxtApp, useStoryblokApi()); + }, +}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/defaultExportSpecifier.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/defaultExportSpecifier.ts new file mode 100644 index 00000000..e36c4286 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/defaultExportSpecifier.ts @@ -0,0 +1,3 @@ +const plugin = () => {}; + +export {plugin as default}; diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/existingDefaultExport.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/existingDefaultExport.ts new file mode 100644 index 00000000..76624cc3 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/existingDefaultExport.ts @@ -0,0 +1,3 @@ +const options = {}; + +export default options; diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/importsOnly.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/importsOnly.ts new file mode 100644 index 00000000..1fc7177d --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/importsOnly.ts @@ -0,0 +1,2 @@ +import {withCroct} from '@croct/plug-storyblok/nuxt'; +import {useStoryblokApi} from '@storyblok/vue'; diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/namedExport.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/namedExport.ts new file mode 100644 index 00000000..6bd5f1f1 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/namedExport.ts @@ -0,0 +1,3 @@ +const helper = () => {}; + +export {helper}; diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/namedPluginExport.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/namedPluginExport.ts new file mode 100644 index 00000000..682acc1b --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/namedPluginExport.ts @@ -0,0 +1,3 @@ +import {defineNuxtPlugin} from 'nuxt/app'; + +export const plugin = defineNuxtPlugin(() => {}); diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/stringDefaultExportSpecifier.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/stringDefaultExportSpecifier.ts new file mode 100644 index 00000000..ddcc58d5 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/stringDefaultExportSpecifier.ts @@ -0,0 +1,3 @@ +const plugin = () => {}; + +export {plugin as 'default'}; diff --git a/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/unrelatedCalls.ts b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/unrelatedCalls.ts new file mode 100644 index 00000000..aac10d78 --- /dev/null +++ b/test/application/project/code/transformation/fixtures/nuxt-storyblok-plugin/unrelatedCalls.ts @@ -0,0 +1,2 @@ +console.log('Croct'); +setup(); 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 index ed4ae8ab..3bfda681 100644 --- a/test/application/project/code/transformation/javascript/__snapshots__/nuxtStoryblokPluginCodemod.test.ts.snap +++ b/test/application/project/code/transformation/javascript/__snapshots__/nuxtStoryblokPluginCodemod.test.ts.snap @@ -20,16 +20,91 @@ export default defineNuxtPlugin(nuxtApp => { " `; +exports[`NuxtStoryblokPluginCodemod should correctly transform alreadyScaffoldedObjectSyntax.ts: alreadyScaffoldedObjectSyntax.ts 1`] = ` +"import { withCroct } from '@croct/plug-storyblok/nuxt'; +import { useStoryblokApi } from '@storyblok/vue'; +import { defineNuxtPlugin } from '#app'; + +export default defineNuxtPlugin({ + name: 'croct-storyblok', + enforce: 'post', + setup(nuxtApp) { + withCroct(nuxtApp, useStoryblokApi()); + }, +}); +" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform defaultExportSpecifier.ts: defaultExportSpecifier.ts 1`] = ` +"const plugin = () => {}; + +export {plugin as default}; +" +`; + 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()); +export default defineNuxtPlugin({ + name: "croct-storyblok", + enforce: "post", + + setup(nuxtApp) { + withCroct(nuxtApp, useStoryblokApi()); + } });" `; +exports[`NuxtStoryblokPluginCodemod should correctly transform existingDefaultExport.ts: existingDefaultExport.ts 1`] = ` +"const options = {}; + +export default options; +" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform importsOnly.ts: importsOnly.ts 1`] = ` +"import { defineNuxtPlugin } from "#app"; +import {withCroct} from '@croct/plug-storyblok/nuxt'; +import {useStoryblokApi} from '@storyblok/vue'; + +export default defineNuxtPlugin({ + name: "croct-storyblok", + enforce: "post", + + setup(nuxtApp) { + withCroct(nuxtApp, useStoryblokApi()); + } +}); +" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform namedExport.ts: namedExport.ts 1`] = ` +"import { defineNuxtPlugin } from "#app"; +import { useStoryblokApi } from "@storyblok/vue"; +import { withCroct } from "@croct/plug-storyblok/nuxt"; +const helper = () => {}; +export {helper}; + +export default defineNuxtPlugin({ + name: "croct-storyblok", + enforce: "post", + + setup(nuxtApp) { + withCroct(nuxtApp, useStoryblokApi()); + } +}); +" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform namedPluginExport.ts: namedPluginExport.ts 1`] = ` +"import {defineNuxtPlugin} from 'nuxt/app'; + +export const plugin = defineNuxtPlugin(() => {}); +" +`; + exports[`NuxtStoryblokPluginCodemod should correctly transform partialScaffold.ts: partialScaffold.ts 1`] = ` "import { defineNuxtPlugin } from '#app'; @@ -39,13 +114,43 @@ export default defineNuxtPlugin(nuxtApp => { " `; +exports[`NuxtStoryblokPluginCodemod should correctly transform stringDefaultExportSpecifier.ts: stringDefaultExportSpecifier.ts 1`] = ` +"const plugin = () => {}; + +export {plugin as 'default'}; +" +`; + +exports[`NuxtStoryblokPluginCodemod should correctly transform unrelatedCalls.ts: unrelatedCalls.ts 1`] = ` +"import { defineNuxtPlugin } from "#app"; +import { useStoryblokApi } from "@storyblok/vue"; +import { withCroct } from "@croct/plug-storyblok/nuxt"; +console.log('Croct'); +setup(); + +export default defineNuxtPlugin({ + name: "croct-storyblok", + enforce: "post", + + setup(nuxtApp) { + withCroct(nuxtApp, useStoryblokApi()); + } +}); +" +`; + 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()); +export default defineNuxtPlugin({ + name: "croct-storyblok", + enforce: "post", + + setup(nuxtApp) { + withCroct(nuxtApp, useStoryblokApi()); + } }); " `; diff --git a/test/application/project/code/transformation/javascript/nuxtConfigParser.test.ts b/test/application/project/code/transformation/javascript/nuxtConfigParser.test.ts new file mode 100644 index 00000000..8cfc9f2a --- /dev/null +++ b/test/application/project/code/transformation/javascript/nuxtConfigParser.test.ts @@ -0,0 +1,48 @@ +import {resolve} from 'path'; +import type {NuxtConfig} from '@/application/project/code/transformation/javascript/nuxtConfigParser'; +import {NuxtConfigParser} from '@/application/project/code/transformation/javascript/nuxtConfigParser'; +import {loadFixtures} from '../fixtures'; + +describe('NuxtConfigParser', () => { + const layoutOptions: NuxtConfig = { + srcDir: 'src', + future: { + compatibilityVersion: 4, + }, + }; + + // Fixtures without an expectation resolve no layout option + const scenarios = loadFixtures( + resolve(__dirname, '../fixtures/nuxt-config'), + {}, + { + 'aliasedDefineCall.ts': layoutOptions, + 'bareObjectExport.ts': layoutOptions, + 'compatibilityVersionOnly.ts': { + future: { + compatibilityVersion: 4, + }, + }, + 'constantReferences.ts': layoutOptions, + 'defineCall.ts': layoutOptions, + 'duplicateProperties.ts': layoutOptions, + 'ignoredProperties.ts': layoutOptions, + 'indirectConfigVariable.ts': layoutOptions, + 'nonObjectFuture.ts': { + srcDir: 'src', + }, + 'srcDirOnly.ts': { + srcDir: 'src', + }, + 'stringKeys.ts': layoutOptions, + 'templateLiteral.ts': layoutOptions, + 'withTypeAssertion.ts': layoutOptions, + }, + ); + + it.each(scenarios)('should correctly parse $name', ({fixture, options: expected}) => { + const parser = new NuxtConfigParser(); + + expect(parser.parse(fixture)).toEqual(expected); + }); +}); diff --git a/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts b/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts index 20e68e05..a6b3a2f5 100644 --- a/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts +++ b/test/application/project/code/transformation/javascript/nuxtStoryblokPluginCodemod.test.ts @@ -14,6 +14,7 @@ describe('NuxtStoryblokPluginCodemod', () => { module: '@croct/plug-storyblok/nuxt', factory: 'withCroct', }, + pluginName: 'croct-storyblok', storyblokVueModule: '@storyblok/vue', nuxtAppModule: '#app', };