diff --git a/package-lock.json b/package-lock.json index 07726eed..790b93b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "croct", "version": "0.0.0-dev", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/core": "^7.28.5", @@ -2531,10 +2532,11 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -10474,10 +10476,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -10778,10 +10781,11 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -13286,10 +13290,11 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", diff --git a/package.json b/package.json index ccccc523..ce2f47f6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "test": "jest -c jest.config.js --coverage", "validate": "tsc --noEmit", "build": "tsup", - "graphql-codegen": "graphql-codegen --config codegen.ts" + "graphql-codegen": "graphql-codegen --config codegen.ts", + "postinstall": "graphql-codegen" }, "dependencies": { "@babel/core": "^7.28.5", diff --git a/src/application/project/code/transformation/javascript/storyblokInitCodemod.ts b/src/application/project/code/transformation/javascript/storyblokInitCodemod.ts new file mode 100644 index 00000000..251f0608 --- /dev/null +++ b/src/application/project/code/transformation/javascript/storyblokInitCodemod.ts @@ -0,0 +1,97 @@ +/* eslint-disable no-param-reassign -- False positives */ +import * as t from '@babel/types'; +import {traverse} from '@babel/core'; +import {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 StoryblokInitCodemodOptions = CodemodOptions & { + name: string, + module: string, +}; + +export class StoryblokInitCodemod implements Codemod { + public apply(input: t.File, options?: StoryblokInitCodemodOptions): Promise> { + if (options === undefined) { + return Promise.resolve({ + modified: false, + result: input, + }); + } + + const localName = getImportLocalName(input, { + moduleName: /@storyblok\/(js|react)/, + importName: /storyblokInit|\*/, + }); + + if (localName === null) { + return Promise.resolve({ + modified: false, + result: input, + }); + } + + const importLocalName = options.module !== undefined + ? getImportLocalName(input, { + importName: options.name, + moduleName: options.module, + }) + : null; + + const wrapperName = importLocalName ?? options.name; + let modified = false; + + traverse(input, { + CallExpression: path => { + const {callee} = path.node; + + // Match direct function calls: targetFunction(...) + if (t.isIdentifier(callee) && callee.name === 'storyblokInit') { + path.node.arguments = [ + t.callExpression( + t.identifier(wrapperName), + path.node.arguments, + ), + ]; + + modified = true; + } + + // Match member expression calls: obj.targetFunction(...) + if ( + t.isMemberExpression(callee) + && t.isIdentifier(callee.property) + && callee.property.name === 'storyblokInit' + ) { + path.node.arguments = [ + t.callExpression( + t.identifier(wrapperName), + path.node.arguments, + ), + ]; + + modified = true; + } + }, + }); + + if (modified && importLocalName === null) { + const {body} = input.program; + + if (!t.isImportDeclaration(body[0])) { + body.unshift(t.emptyStatement()); + } + + addImport(input, { + type: 'value', + moduleName: options.module, + importName: options.name, + }); + } + + return Promise.resolve({ + modified: modified, + result: input, + }); + } +} diff --git a/src/application/project/code/transformation/javascript/utils/getImportLocalName.ts b/src/application/project/code/transformation/javascript/utils/getImportLocalName.ts index f28f36d7..efe50180 100644 --- a/src/application/project/code/transformation/javascript/utils/getImportLocalName.ts +++ b/src/application/project/code/transformation/javascript/utils/getImportLocalName.ts @@ -33,6 +33,14 @@ export function getImportLocalName(source: string | t.File, matcher: ImportMatch continue; } + if (t.isImportNamespaceSpecifier(specifier)) { + if (matches('*', matcher.importName)) { + localName = specifier.local.name; + } + + continue; + } + if (t.isImportSpecifier(specifier) && matches(specifier.imported, matcher.importName)) { localName = specifier.local.name; diff --git a/src/application/project/sdk/javasScriptSdk.ts b/src/application/project/sdk/javasScriptSdk.ts index 404f2712..7727f646 100644 --- a/src/application/project/sdk/javasScriptSdk.ts +++ b/src/application/project/sdk/javasScriptSdk.ts @@ -36,6 +36,7 @@ export type Configuration = { formatter: CodeFormatter, fileSystem: FileSystem, tsConfigLoader: TsConfigLoader, + plugins?: JavaScriptSdkPlugin[], }; type VersionedContent = { @@ -49,6 +50,19 @@ type ContentOptions = BaseContentOptions & { notifier?: TaskNotifier, }; +export type JavaScriptPluginContext = { + packageManager: PackageManager, + projectDirectory: WorkingDirectory, + fileSystem: FileSystem, +}; + +export type JavaScriptSdkPlugin = { + getInstallationPlan( + installation: Installation, + context: JavaScriptPluginContext + ): Promise>, +}; + export abstract class JavaScriptSdk implements Sdk { protected static readonly CONTENT_PACKAGE = '@croct/content'; @@ -64,6 +78,8 @@ export abstract class JavaScriptSdk implements Sdk { private readonly importConfigLoader: TsConfigLoader; + private readonly plugins: JavaScriptSdkPlugin[]; + protected constructor(configuration: Configuration) { this.projectDirectory = configuration.projectDirectory; this.packageManager = configuration.packageManager; @@ -71,6 +87,7 @@ export abstract class JavaScriptSdk implements Sdk { this.formatter = configuration.formatter; this.fileSystem = configuration.fileSystem; this.importConfigLoader = configuration.tsConfigLoader; + this.plugins = configuration.plugins ?? []; } public async generateSlotExample(slot: Slot, installation: Installation): Promise { @@ -99,7 +116,7 @@ export abstract class JavaScriptSdk implements Sdk { public async setup(installation: Installation): Promise { const {input, output} = installation; - const plan = await this.getInstallationPlan(installation); + const plan = await this.resolveInstallationPlan(installation); const configuration: ProjectConfiguration = { ...plan.configuration, @@ -279,6 +296,32 @@ export abstract class JavaScriptSdk implements Sdk { return defaultPath; } + private resolveInstallationPlan(installation: Installation): Promise { + let promise = this.getInstallationPlan(installation); + const context: JavaScriptPluginContext = { + packageManager: this.packageManager, + projectDirectory: this.projectDirectory, + fileSystem: this.fileSystem, + }; + + for (const plugin of this.plugins) { + promise = promise.then(async plan => { + const hookPlan = await plugin.getInstallationPlan(installation, context); + + return { + tasks: [...plan.tasks, ...(hookPlan.tasks ?? [])], + dependencies: [...plan.dependencies, ...(hookPlan.dependencies ?? [])], + configuration: { + ...plan.configuration, + ...hookPlan.configuration, + }, + }; + }); + } + + return promise; + } + protected abstract getInstallationPlan(installation: Installation): Promise; public async update(installation: Installation, options: UpdateOptions = {}): Promise { diff --git a/src/application/project/sdk/storyblokPlugin.ts b/src/application/project/sdk/storyblokPlugin.ts new file mode 100644 index 00000000..51ff12c3 --- /dev/null +++ b/src/application/project/sdk/storyblokPlugin.ts @@ -0,0 +1,109 @@ +import {extname} from 'path'; +import {InstallationPlan, JavaScriptSdkPlugin, JavaScriptPluginContext} from '@/application/project/sdk/javasScriptSdk'; +import {Task} from '@/application/cli/io/output'; +import {HelpfulError} from '@/application/error'; +import {Codemod} from '@/application/project/code/transformation/codemod'; +import {Installation} from '@/application/project/sdk/sdk'; +import {ScanFilter} from '@/application/fs/fileSystem'; + +export type Configuration = { + scanFilter: ScanFilter, + codemod: Codemod, +}; + +export class StoryblokPlugin implements JavaScriptSdkPlugin { + private readonly codemod: Codemod; + + private readonly scanFilter: ScanFilter; + + public constructor(configuration: Configuration) { + this.codemod = configuration.codemod; + this.scanFilter = configuration.scanFilter; + } + + public async getInstallationPlan( + _: Installation, + context: JavaScriptPluginContext, + ): Promise> { + if (!await context.packageManager.hasDependency('@storyblok/js')) { + return {}; + } + + const tasks: Task[] = []; + + tasks.push({ + title: 'Configure Storyblok integration', + task: async notifier => { + notifier.update('Configuring Storyblok integration'); + + try { + await this.configureStoryblok(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 configureStoryblok(scope: JavaScriptPluginContext): Promise { + const initializationFiles = await this.findStoryblokInitializationFiles(scope); + + if (initializationFiles.length === 0) { + throw new HelpfulError('Could not find any file containing Storyblok initialization.'); + } + + const results = initializationFiles.map( + file => this.codemod + .apply(file) + .then(result => result.modified), + ); + + if (!(await Promise.all(results)).some(modified => modified)) { + throw new HelpfulError('Could not find any Storyblok initialization to configure.'); + } + } + + private async findStoryblokInitializationFiles(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) { + return false; + } + + const extension = extname(path).toLowerCase(); + + // Allow directories (no extension) and JS/TS files + return extension === '' + || extension === '.js' + || extension === '.ts' + || extension === '.jsx' + || extension === '.tsx'; + }); + + const files: string[] = []; + + for await (const entry of iterator) { + if (entry.type !== 'file') { + continue; + } + + const path = fileSystem.joinPaths(directory, entry.name); + const content = await fileSystem.readTextFile(path); + + if (content.includes('storyblokInit')) { + files.push(path); + } + } + + return files; + } +} diff --git a/src/infrastructure/application/cli/cli.ts b/src/infrastructure/application/cli/cli.ts index 33618783..00d7e034 100644 --- a/src/infrastructure/application/cli/cli.ts +++ b/src/infrastructure/application/cli/cli.ts @@ -7,10 +7,14 @@ import XDGAppPaths from 'xdg-app-paths'; import ci from 'ci-info'; import {FilteredLogger, Logger, LogLevel} from '@croct/logging'; import {Token} from '@croct/sdk/token'; +import {File} from '@babel/types'; import {ConsoleInput} from '@/infrastructure/application/cli/io/consoleInput'; import {ConsoleOutput, LinkOpener} from '@/infrastructure/application/cli/io/consoleOutput'; import {Sdk} from '@/application/project/sdk/sdk'; -import {Configuration as JavaScriptSdkConfiguration} from '@/application/project/sdk/javasScriptSdk'; +import { + Configuration as JavaScriptSdkConfiguration, + JavaScriptSdkPlugin, +} from '@/application/project/sdk/javasScriptSdk'; import {PlugJsSdk} from '@/application/project/sdk/plugJsSdk'; import {PlugReactSdk} from '@/application/project/sdk/plugReactSdk'; import {PlugNextSdk} from '@/application/project/sdk/plugNextSdk'; @@ -312,7 +316,7 @@ import {WriteFileOptionsValidator} from '@/infrastructure/application/validation import {AutoUpdater} from '@/application/cli/autoUpdater'; import {DeletePathAction} from '@/application/template/action/deletePathAction'; import {DeletePathOptionsValidator} from '@/infrastructure/application/validation/actions/deletePathOptionsValidator'; -import {Codemod} from '@/application/project/code/transformation/codemod'; +import {Codemod, ResultCode} from '@/application/project/code/transformation/codemod'; import {ResolveImportAction} from '@/application/template/action/resolveImportAction'; import { ResolveImportOptionsValidator, @@ -321,6 +325,8 @@ import {CreateApiKeyAction} from '@/application/template/action/createApiKeyActi import { CreateApiKeyOptionsValidator, } from '@/infrastructure/application/validation/actions/createApiKeyOptionsValidator'; +import {StoryblokInitCodemod} from '@/application/project/code/transformation/javascript/storyblokInitCodemod'; +import {StoryblokPlugin} from '@/application/project/sdk/storyblokPlugin'; export type Configuration = { program: Program, @@ -1727,10 +1733,12 @@ export class Cli { mapping: { [Platform.JAVASCRIPT]: (): Sdk => new PlugJsSdk({ ...config, + plugins: [this.createStoryblokPlugin(Platform.JAVASCRIPT)], bundlers: ['vite', 'parcel', 'tsup', 'rollup'], }), [Platform.REACT]: (): Sdk => new PlugReactSdk({ ...config, + plugins: [this.createStoryblokPlugin(Platform.REACT)], importResolver: importResolver, codemod: { provider: new FormatCodemod( @@ -1790,6 +1798,7 @@ export class Cli { return new PlugNextSdk({ ...config, + plugins: [this.createStoryblokPlugin(Platform.NEXTJS)], userApi: this.getUserApi(), applicationApi: this.getApplicationApi(), importResolver: importResolver, @@ -1923,6 +1932,36 @@ export class Cli { }); } + private createStoryblokPlugin( + platform: Platform.JAVASCRIPT | Platform.REACT | Platform.NEXTJS, + ): JavaScriptSdkPlugin { + const codemod = new StoryblokInitCodemod(); + const modules = { + [Platform.JAVASCRIPT]: 'js', + [Platform.REACT]: 'react', + [Platform.NEXTJS]: 'next', + }; + + return new StoryblokPlugin({ + scanFilter: this.getScanFilter(), + codemod: new FormatCodemod( + this.getJavaScriptFormatter(), + new FileCodemod({ + fileSystem: this.getFileSystem(), + codemod: new JavaScriptCodemod({ + languages: ['typescript', 'jsx'], + codemod: { + apply: (input: File): Promise> => codemod.apply(input, { + name: 'withCroct', + module: `@croct/plug-storyblok/${modules[platform]}`, + }), + }, + }), + }), + ), + }); + } + private share any)>(method: M, factory: () => ReturnType): ReturnType { const instance = this.instances.get(method); diff --git a/test/application/project/code/transformation/javascript/storyblokInitCodemod.test.ts b/test/application/project/code/transformation/javascript/storyblokInitCodemod.test.ts new file mode 100644 index 00000000..7ebd38aa --- /dev/null +++ b/test/application/project/code/transformation/javascript/storyblokInitCodemod.test.ts @@ -0,0 +1,221 @@ +import { + StoryblokInitCodemod, + StoryblokInitCodemodOptions, +} from '@/application/project/code/transformation/javascript/storyblokInitCodemod'; +import {JavaScriptCodemod} from '@/application/project/code/transformation/javascript/javaScriptCodemod'; + +describe('StoryblokInitCodemod', () => { + function createTransformer(): JavaScriptCodemod { + return new JavaScriptCodemod({ + languages: ['typescript', 'jsx'], + codemod: new StoryblokInitCodemod(), + }); + } + + it('should wrap storyblokInit arguments when imported from @storyblok/js', async () => { + const transformer = createTransformer(); + + const input = [ + "import { storyblokInit } from '@storyblok/js';", + '', + 'storyblokInit({ accessToken: "token" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(true); + expect(result).toEqual([ + 'import { withCroct } from "@croct/storyblok";', + "import { storyblokInit } from '@storyblok/js';", + 'storyblokInit(withCroct({ accessToken: "token" }));', + ].join('\n')); + }); + + it('should wrap storyblokInit arguments when imported from @storyblok/react', async () => { + const transformer = createTransformer(); + + const input = [ + "import { storyblokInit } from '@storyblok/react';", + '', + 'storyblokInit({ accessToken: "token" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(true); + expect(result).toEqual([ + 'import { withCroct } from "@croct/storyblok";', + "import { storyblokInit } from '@storyblok/react';", + 'storyblokInit(withCroct({ accessToken: "token" }));', + ].join('\n')); + }); + + it('should wrap member expression calls', async () => { + const transformer = createTransformer(); + + const input = [ + "import * as sb from '@storyblok/js';", + '', + 'sb.storyblokInit({ accessToken: "token" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(true); + expect(result).toEqual([ + 'import { withCroct } from "@croct/storyblok";', + "import * as sb from '@storyblok/js';", + 'sb.storyblokInit(withCroct({ accessToken: "token" }));', + ].join('\n')); + }); + + it('should use existing import alias for wrapper function', async () => { + const transformer = createTransformer(); + + const input = [ + "import { withCroct as croctWrapper } from '@croct/storyblok';", + "import { storyblokInit } from '@storyblok/js';", + '', + 'storyblokInit({ accessToken: "token" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(true); + expect(result).toEqual([ + "import { withCroct as croctWrapper } from '@croct/storyblok';", + "import { storyblokInit } from '@storyblok/js';", + '', + 'storyblokInit(croctWrapper({ accessToken: "token" }));', + ].join('\n')); + }); + + it('should not modify code when storyblokInit import is not found', async () => { + const transformer = createTransformer(); + + const input = [ + "import { someOtherFunction } from '@storyblok/js';", + '', + 'someOtherFunction({ accessToken: "token" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(false); + expect(result).toBe(input); + }); + + it('should not modify code when no storyblok import exists', async () => { + const transformer = createTransformer(); + + const input = [ + "import { something } from 'other-module';", + '', + 'storyblokInit({ accessToken: "token" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(false); + expect(result).toBe(input); + }); + + it('should return unmodified when options are not provided', async () => { + const transformer = createTransformer(); + + const input = [ + "import { storyblokInit } from '@storyblok/js';", + '', + 'storyblokInit({ accessToken: "token" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input); + + expect(modified).toBe(false); + expect(result).toBe(input); + }); + + it('should wrap multiple storyblokInit calls', async () => { + const transformer = createTransformer(); + + const input = [ + "import { storyblokInit } from '@storyblok/js';", + '', + 'storyblokInit({ accessToken: "token1" });', + 'storyblokInit({ accessToken: "token2" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(true); + expect(result).toEqual([ + 'import { withCroct } from "@croct/storyblok";', + "import { storyblokInit } from '@storyblok/js';", + 'storyblokInit(withCroct({ accessToken: "token1" }));', + 'storyblokInit(withCroct({ accessToken: "token2" }));', + ].join('\n')); + }); + + it('should handle storyblokInit with multiple arguments', async () => { + const transformer = createTransformer(); + + const input = [ + "import { storyblokInit } from '@storyblok/js';", + '', + 'storyblokInit(config, options);', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(true); + expect(result).toEqual([ + 'import { withCroct } from "@croct/storyblok";', + "import { storyblokInit } from '@storyblok/js';", + 'storyblokInit(withCroct(config, options));', + ].join('\n')); + }); + + it('should add empty statement before import when first statement is not an import', async () => { + const transformer = createTransformer(); + + const input = [ + 'const x = 1;', + "import { storyblokInit } from '@storyblok/js';", + '', + 'storyblokInit({ accessToken: "token" });', + ].join('\n'); + + const {result, modified} = await transformer.apply(input, { + name: 'withCroct', + module: '@croct/storyblok', + }); + + expect(modified).toBe(true); + expect(result).toContain('import { withCroct } from "@croct/storyblok";'); + expect(result).toContain('storyblokInit(withCroct({ accessToken: "token" }));'); + }); +}); diff --git a/test/application/project/code/transformation/javascript/utils/getImportLocalName.test.ts b/test/application/project/code/transformation/javascript/utils/getImportLocalName.test.ts index 2b82454c..e90f010f 100644 --- a/test/application/project/code/transformation/javascript/utils/getImportLocalName.test.ts +++ b/test/application/project/code/transformation/javascript/utils/getImportLocalName.test.ts @@ -93,6 +93,42 @@ describe('getImportLocalName', () => { }, expected: 'alias', }, + { + description: 'return the local name of the namespace import', + code: 'import * as sdk from \'croct\';', + matcher: { + moduleName: 'croct', + importName: '*', + }, + expected: 'sdk', + }, + { + description: 'return null if namespace import does not match the module name', + code: 'import * as sdk from \'croct\';', + matcher: { + moduleName: 'something', + importName: '*', + }, + expected: null, + }, + { + description: 'return null if looking for namespace but import is named', + code: 'import {sdk} from \'croct\';', + matcher: { + moduleName: 'croct', + importName: '*', + }, + expected: null, + }, + { + description: 'return the local name of the namespace import when module matches regex', + code: 'import * as croct from \'@croct/sdk\';', + matcher: { + moduleName: /@croct/, + importName: '*', + }, + expected: 'croct', + }, ])('should $description', ({code, matcher, expected}) => { expect(getImportLocalName(code, matcher)).toBe(expected); });