diff --git a/README.md b/README.md index 31b77114..a4b1f9be 100644 --- a/README.md +++ b/README.md @@ -67,27 +67,24 @@ An exemplary project configuration (`.velocitas.json`) looks like this: ```json { - "packages": [ - { - "repo": "package-A", - "version": "v1.0.0" - }, - { - "repo": "package-B", - "version": "v2.3.1-dev" - } - ], + "packages": { + "package-A": "v1.0.0", + "package-B": "v2.3.1-dev" + }, + "components": [ "component-A", "component-B" ], "variables": { "repoUrl": "https://github.com/eclipse-velocitas/cli", "copyrightYear": 2023, - "autoGenerateVehicleModel": true + "autoGenerateVehicleModel": true, + "variableA@package-A": "variableA", + "variableB@component-B": "variableB", } } ``` -As mentioned previously, a package simply is a git repository. The `repo` attribute of a package is used to identify the git repository which holds the package. `repo` is currently resolved to `https://github.com/eclipse-velocitas/`. Alternatively, you can also supply a fully qualified Git repo URL e.g. `https:///.git` or `git@/.git`. Credentials for HTTPs and SSH based git repos are provided by your local git configuration (CLI is using Git under the hood). The `version` attribute specifies a tag, a branch or a SHA of the repository. +As mentioned previously, a package simply is a git repository. The key inside the packages is used to identify the git repository which holds the package. It is currently resolved to `https://github.com/eclipse-velocitas/`. Alternatively, you can also supply a fully qualified Git repo URL e.g. `https:///.git` or `git@/.git`. Credentials for HTTPs and SSH based git repos are provided by your local git configuration (CLI is using Git under the hood). The value of the package attribute specifies a tag, a branch or a SHA of the repository. -The `variables` block holds user configured values for the packages and their contained components. It is a global variable definition. Should two components share the same variable name, both can be set with one line in this global block. Package-wide or component-wide variable configuration to avoid name clashes is also possible. +The `variables` block holds configured values for a specific scope (project, package or component). A variable without separator acts as a global variable. Should two components share the same variable name, both can be set with one line in this global block. Package-wide or component-wide variable configuration can be used to avoid name clashes. For a package or component scope the variable needs to be assigned with an '@' followed by either package or component ID. In the example above, `variableA@package-A` and `variableB@component-B` showcase such a usage. Click [here](./docs/PROJECT-CONFIG.md) for an in-depth overview of the project configuration. diff --git a/docs/PROJECT-CONFIG.md b/docs/PROJECT-CONFIG.md index 970a845c..06a108e4 100644 --- a/docs/PROJECT-CONFIG.md +++ b/docs/PROJECT-CONFIG.md @@ -1,25 +1,22 @@ # Project configuration -The project configuration describes which packages your project is using and in which version. The versions of the referenced packages can be upgraded using the `upgrade` command. If you only want to see which new versions are available use `upgrade --dry-run` or `upgrade --dry-run --ignore-bounds`. Each package may expose variables which need to be set from the project configuration. If multiple different packages all expose the same named variable `foo`, setting this variable once in the project configuration will pass the value to all packages. +The project configuration describes which packages your project is using and in which version. The versions of the referenced packages can be upgraded using the `upgrade` command. If you only want to see which new versions are available use `upgrade --dry-run` or `upgrade --dry-run --ignore-bounds`. Each package may expose variables which need to be set from the project configuration. If multiple different packages all expose the same named variable `foo`, setting this variable once in the project configuration will pass the value to all packages. If a package or even a component exposes a variable which is only needed within its scope it can be set with `"variableA@package-A": "variableA"`. Read more about variables [here](./features/VARIABLES.md). ```json { - "packages": [ - { - "repo": "package-A", - "version": "v1.0.0" - }, - { - "repo": "package-B", - "version": "v2.3.1-dev" - } - ], + "packages": { + "package-A": "v1.0.0", + "package-B": "v2.3.1-dev" + }, + "components": [ "component-A", "component-B" ], "variables": { "repoUrl": "https://github.com/eclipse-velocitas/cli", "copyrightYear": 2023, - "autoGenerateVehicleModel": true + "autoGenerateVehicleModel": true, + "variableA@package-A": "variableA", + "variableB@component-B": "variableB", } } ``` @@ -32,24 +29,11 @@ By default, all components of a package will be used, but if desired the used co ```json { - "packages": [ - { - "repo": "package-A", - "version": "v1.0.0" - }, - { - "repo": "package-B", - "version": "v2.3.1-dev" - } - ], - "components": [ - { - "id": "component-exposed-by-pkg-a" - }, - { - "id": "component-exposed-by-pkg-b" - }, - ], + "packages": { + "package-A": "v1.0.0", + "package-B": "v2.3.1-dev" + }, + "components": [ "component-exposed-by-pkg-a", "component-exposed-by-pkg-b" ], "variables": { "repoUrl": "https://github.com/eclipse-velocitas/cli", "copyrightYear": 2023, @@ -62,23 +46,25 @@ The project above will only use the components `component-exposed-by-pkg-a` and ## File Structure -### `packages` - Array[[`PackageConfig`](#packageconfig)] +### `packages` - Map[string, string] + +A key-value configuration of packages, where key is the package [repo](#repo) and value is the [version](#version). -Array of packages used in the project. +### `components` - string[] + +An array of used components. ### `variables` - Map[string, any] -Project-wide key-value variable configuration. +Project-wide key-value [variable](#variables) configuration. # Types -## `PackageConfig` - -### `repo` - string +## `repo` The name of the package or URL to the package git repository. A simple name is currently resolved to `https://github.com/eclipse-velocitas/`. Alternatively, you can also supply a fully qualified Git repo URL e.g. `https:///.git` or `git@/.git`. Credentials for HTTPs and SSH based git repos are provided by your local git configuration (CLI is using Git under the hood). -### `version` - string +## `version` The version of the package to use. | Literal | Behaviour | Example | @@ -88,20 +74,10 @@ The version of the package to use. | branch (prefixed with an '@') | Refers to the latest commit in a specific branch | `"@main"` | | latest | Refers to the latest tag if available else to the highest version tag | `"latest"` | -### `variables` - Map[string, any] - -Package-wide variable configuration. - -### `components` - Array[[`ComponentConfig`](#componentconfig)] - -Per-component configuration. - -## `ComponentConfig` - -### `id` - string - -Unique ID of the component within the package. - -### `variables` - Map[string, any] +## `variables` -Component-wide key-value variable configuration +| Scope | Example | +|-------|---------| +| Global | `"projectVariable": "A project wide variable"` | +| Package | `"packageVariable@pkg-a": "A package wide variable"` | +| Component | `"componentVariable@component-exposed-by-pkg-a": "A component wide variable"` | diff --git a/src/commands/cache/clear.ts b/src/commands/cache/clear.ts index 99bcf955..0a8ddb3b 100644 --- a/src/commands/cache/clear.ts +++ b/src/commands/cache/clear.ts @@ -14,7 +14,7 @@ import { Command } from '@oclif/core'; import { ProjectCache } from '../../modules/project-cache'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; export default class Clear extends Command { static description = "Clean a project's cache."; @@ -26,7 +26,7 @@ export default class Clear extends Command { // although we are not reading the project config, we want to // ensure the command is run in a project directory only. - ProjectConfig.read(`v${this.config.version}`); + ProjectConfigIO.read(`v${this.config.version}`); const cache = ProjectCache.read(); cache.clear(); cache.write(); diff --git a/src/commands/cache/get.ts b/src/commands/cache/get.ts index ab93a9ee..ccef47d4 100644 --- a/src/commands/cache/get.ts +++ b/src/commands/cache/get.ts @@ -15,7 +15,7 @@ import { Args, Command } from '@oclif/core'; import { mapReplacer } from '../../modules/helpers'; import { ProjectCache } from '../../modules/project-cache'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; export default class Get extends Command { static description = 'Get the complete cache contents as JSON string or the value of a single key.'; @@ -36,7 +36,7 @@ bar`, // although we are not reading the project config, we want to // ensure the command is run in a project directory only. - ProjectConfig.read(`v${this.config.version}`); + ProjectConfigIO.read(`v${this.config.version}`); const cache = ProjectCache.read(); diff --git a/src/commands/cache/set.ts b/src/commands/cache/set.ts index 634c7508..8ec7b291 100644 --- a/src/commands/cache/set.ts +++ b/src/commands/cache/set.ts @@ -14,7 +14,7 @@ import { Args, Command } from '@oclif/core'; import { ProjectCache } from '../../modules/project-cache'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; export default class Set extends Command { static description = 'Set the cache value of an entry.'; @@ -31,7 +31,7 @@ export default class Set extends Command { // although we are not reading the project config, we want to // ensure the command is run in a project directory only. - ProjectConfig.read(`v${this.config.version}`); + ProjectConfigIO.read(`v${this.config.version}`); const cache = ProjectCache.read(); cache.set(args.key, args.value); diff --git a/src/commands/component/add.ts b/src/commands/component/add.ts index 078b1934..25677b79 100644 --- a/src/commands/component/add.ts +++ b/src/commands/component/add.ts @@ -13,7 +13,8 @@ // SPDX-License-Identifier: Apache-2.0 import { Args, Command } from '@oclif/core'; -import { ProjectConfig } from '../../modules/project-config'; +import { ComponentContext } from '../../modules/component'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; export default class Add extends Command { static description = 'Add project components.'; @@ -27,8 +28,10 @@ export default class Add extends Command { async run(): Promise { const { args } = await this.parse(Add); - const projectConfig = ProjectConfig.read(`v${this.config.version}`); - const foundComponent = projectConfig.getComponents(false).find((compContext) => compContext.manifest.id === args.id); + const projectConfig = ProjectConfigIO.read(`v${this.config.version}`); + const foundComponent = projectConfig + .getComponentContexts(false) + .find((compContext: ComponentContext) => compContext.manifest.id === args.id); if (!foundComponent) { throw Error( @@ -41,6 +44,6 @@ export default class Add extends Command { } projectConfig.addComponent(foundComponent?.manifest.id); - projectConfig.write(); + ProjectConfigIO.write(projectConfig); } } diff --git a/src/commands/component/list.ts b/src/commands/component/list.ts index 5097d7ed..288914a7 100644 --- a/src/commands/component/list.ts +++ b/src/commands/component/list.ts @@ -13,7 +13,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Command, Flags } from '@oclif/core'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; export default class List extends Command { static description = 'List project components.'; @@ -35,10 +35,10 @@ export default class List extends Command { async run(): Promise { const { flags } = await this.parse(List); - const projectConfig = ProjectConfig.read(`v${this.config.version}`); + const projectConfig = ProjectConfigIO.read(`v${this.config.version}`); const onlyUsed = !flags.all && !flags.unused; - for (const componentContext of projectConfig.getComponents(onlyUsed)) { + for (const componentContext of projectConfig.getComponentContexts(onlyUsed)) { if (flags.unused && componentContext.usedInProject) { continue; } diff --git a/src/commands/component/remove.ts b/src/commands/component/remove.ts index 4f700229..3c86672a 100644 --- a/src/commands/component/remove.ts +++ b/src/commands/component/remove.ts @@ -13,7 +13,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Args, Command } from '@oclif/core'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; export default class Remove extends Command { static description = 'Remove project components.'; @@ -21,15 +21,15 @@ export default class Remove extends Command { static examples = [`$ velocitas component remove `]; static args = { - id: Args.string({ description: 'ID of the component to add', required: true }), + id: Args.string({ description: 'ID of the component to remove', required: true }), }; async run(): Promise { const { args } = await this.parse(Remove); - const projectConfig = ProjectConfig.read(`v${this.config.version}`); + const projectConfig = ProjectConfigIO.read(`v${this.config.version}`); - const foundComponent = projectConfig.getComponents(false).find((compContext) => compContext.manifest.id === args.id); + const foundComponent = projectConfig.getComponentContexts(false).find((compContext) => compContext.manifest.id === args.id); if (!foundComponent) { throw Error( @@ -42,6 +42,6 @@ export default class Remove extends Command { } projectConfig.removeComponent(foundComponent?.manifest.id); - projectConfig.write(); + ProjectConfigIO.write(projectConfig); } } diff --git a/src/commands/create/index.ts b/src/commands/create/index.ts index 6f31410e..bb95df91 100644 --- a/src/commands/create/index.ts +++ b/src/commands/create/index.ts @@ -16,7 +16,8 @@ import { Command, Flags } from '@oclif/core'; import { AppManifest, AppManifestInterfaceAttributes } from '../../modules/app-manifest'; import { InteractiveMode } from '../../modules/create-interactive'; import { CoreComponent, CoreOptions, DescribedId, ExtensionComponent, PackageIndex, Parameter } from '../../modules/package-index'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfig } from '../../modules/projectConfig/projectConfig'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; // eslint-disable-next-line @typescript-eslint/naming-convention import Exec from '../exec'; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -228,7 +229,9 @@ export default class Create extends Command { } else { createData = await this._parseFlags(packageIndex, flags); } - await ProjectConfig.create(createData.componentIds, packageIndex, this.config.version); + const projectConfig = await ProjectConfig.create(createData.componentIds, packageIndex, this.config.version); + ProjectConfigIO.write(projectConfig); + createData.appManifest.write(); this.log(`... Project for Vehicle Application '${createData.name}' created!`); diff --git a/src/commands/exec/index.ts b/src/commands/exec/index.ts index d9c0ab36..fb45ed8a 100644 --- a/src/commands/exec/index.ts +++ b/src/commands/exec/index.ts @@ -16,7 +16,7 @@ import { Args, Command, Flags } from '@oclif/core'; import { APP_MANIFEST_PATH_VARIABLE, AppManifest } from '../../modules/app-manifest'; import { ExecSpec } from '../../modules/component'; import { ExecExitError, runExecSpec } from '../../modules/exec'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; import { createEnvVars } from '../../modules/variables'; export default class Exec extends Command { @@ -75,7 +75,7 @@ export default class Exec extends Command { const programArgsAndFlags = this._extractProgramArgsAndFlags(); const { args, flags } = await this.parse(Exec); - const projectConfig = ProjectConfig.read(`v${this.config.version}`); + const projectConfig = ProjectConfigIO.read(`v${this.config.version}`); const execSpec: ExecSpec = { ref: args.ref, diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index b5067888..c95f4912 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -17,7 +17,8 @@ import { APP_MANIFEST_PATH_VARIABLE, AppManifest } from '../../modules/app-manif import { ComponentContext, ExecSpec } from '../../modules/component'; import { ExecExitError, runExecSpec } from '../../modules/exec'; import { PackageConfig } from '../../modules/package'; -import { ProjectConfig, ProjectConfigLock } from '../../modules/project-config'; +import { ProjectConfig } from '../../modules/projectConfig/projectConfig'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; import { resolveVersionIdentifier } from '../../modules/semver'; import { createEnvVars } from '../../modules/variables'; @@ -102,7 +103,7 @@ export default class Init extends Command { projectConfig.validateUsedComponents(); if (!flags['no-hooks']) { - await this._runPostInitHooks(projectConfig.getComponents(), projectConfig, flags.verbose); + await this._runPostInitHooks(projectConfig.getComponentContexts(), projectConfig, flags.verbose); } } @@ -128,13 +129,17 @@ export default class Init extends Command { this._finalizeSinglePackageInit(requestedPackageConfig, projectConfig); if (!flags['no-hooks']) { - await this._runPostInitHooks(projectConfig.getComponentsForPackageConfig(requestedPackageConfig), projectConfig, flags.verbose); + await this._runPostInitHooks( + projectConfig.getComponentContextsForPackageConfig(requestedPackageConfig), + projectConfig, + flags.verbose, + ); } } private _finalizeSinglePackageInit(requestedPackageConfig: PackageConfig, projectConfig: ProjectConfig): void { const providedComponents = requestedPackageConfig.readPackageManifest().components; - const enabledComponentIds = projectConfig.getComponents(undefined, true).map((comp) => comp.config.id); + const enabledComponentIds = projectConfig.getComponentContexts(undefined, true).map((comp) => comp.config.id); const areComponentsExisting = providedComponents.some((comp) => enabledComponentIds.includes(comp.id)); if (!areComponentsExisting) { @@ -143,18 +148,18 @@ export default class Init extends Command { }); } - projectConfig.write(); + ProjectConfigIO.write(projectConfig); } private _initializeOrReadProject(): ProjectConfig { let projectConfig: ProjectConfig; - if (!ProjectConfig.isAvailable()) { + if (!ProjectConfigIO.isConfigAvailable()) { this.log('... Directory is no velocitas project. Creating .velocitas.json at the root of your repository.'); projectConfig = new ProjectConfig(`v${this.config.version}`); - projectConfig.write(); + ProjectConfigIO.write(projectConfig); } else { - projectConfig = ProjectConfig.read(`v${this.config.version}`, undefined, true); + projectConfig = ProjectConfigIO.read(`v${this.config.version}`, undefined, true); } return projectConfig; } @@ -238,9 +243,9 @@ export default class Init extends Command { } private _createProjectLockFile(projectConfig: ProjectConfig, verbose: boolean): void { - if (verbose && !ProjectConfigLock.isAvailable()) { + if (verbose && !ProjectConfigIO.isLockAvailable()) { this.log('... No .velocitas-lock.json found. Creating it at the root of your repository.'); } - ProjectConfigLock.write(projectConfig); + ProjectConfigIO.writeLock(projectConfig); } } diff --git a/src/commands/package/index.ts b/src/commands/package/index.ts index 4a267227..86e2881e 100644 --- a/src/commands/package/index.ts +++ b/src/commands/package/index.ts @@ -15,7 +15,7 @@ import { Args, Command, Flags } from '@oclif/core'; import { join } from 'node:path'; import { PackageConfig } from '../../modules/package'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; export default class Package extends Command { static description = 'Prints information about packages'; @@ -47,7 +47,7 @@ export default class Package extends Command { async run(): Promise { const { args, flags } = await this.parse(Package); - const projectConfig = ProjectConfig.read(`v${this.config.version}`); + const projectConfig = ProjectConfigIO.read(`v${this.config.version}`); let packagesToPrint: PackageConfig[] = []; diff --git a/src/commands/sync/index.ts b/src/commands/sync/index.ts index d8bf240f..83e3b9b7 100644 --- a/src/commands/sync/index.ts +++ b/src/commands/sync/index.ts @@ -13,7 +13,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Command } from '@oclif/core'; -import { ProjectConfig } from '../../modules/project-config'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; import { installComponent } from '../../modules/setup'; export default class Sync extends Command { @@ -28,9 +28,9 @@ Syncing Velocitas components! async run(): Promise { this.log(`Syncing Velocitas components!`); - const projectConfig = ProjectConfig.read(`v${this.config.version}`); + const projectConfig = ProjectConfigIO.read(`v${this.config.version}`); - for (const component of projectConfig.getComponents()) { + for (const component of projectConfig.getComponentContexts()) { if (!component.manifest.files || component.manifest.files.length === 0) { continue; } diff --git a/src/commands/upgrade/index.ts b/src/commands/upgrade/index.ts index fffa7026..89019408 100644 --- a/src/commands/upgrade/index.ts +++ b/src/commands/upgrade/index.ts @@ -14,7 +14,9 @@ import { Command, Flags } from '@oclif/core'; import { PackageConfig } from '../../modules/package'; -import { ProjectConfig, ProjectConfigLock } from '../../modules/project-config'; +import { ProjectConfig } from '../../modules/projectConfig/projectConfig'; +import { ProjectConfigIO } from '../../modules/projectConfig/projectConfigIO'; +import { ProjectConfigLock } from '../../modules/projectConfig/projectConfigLock'; import { getLatestVersion, incrementVersionRange, resolveVersionIdentifier } from '../../modules/semver'; // eslint-disable-next-line @typescript-eslint/naming-convention import Init from '../init'; @@ -44,15 +46,14 @@ export default class Upgrade extends Command { async run(): Promise { const { flags } = await this.parse(Upgrade); - let projectConfigLock: ProjectConfigLock | null = ProjectConfigLock.read(); + let projectConfigLock: ProjectConfigLock | null = ProjectConfigIO.readLock(); if (!projectConfigLock) { throw new Error(`No .velocitas-lock.json found. Please 'velocitas init' first!`); } this.log(`Checking .velocitas.json for updates!`); - const projectConfig = ProjectConfig.read(`v${this.config.version}`, undefined, true); - + const projectConfig = ProjectConfigIO.read(`v${this.config.version}`, undefined, true); let isAnyPackageUpdated: boolean = false; try { for (const packageConfig of projectConfig.getPackages()) { @@ -93,7 +94,7 @@ export default class Upgrade extends Command { return false; } else { packageConfig.setPackageVersion(incrementVersionRange(initialVersionSpecifier, matchedVersion)); - projectConfig.write(); + ProjectConfigIO.write(projectConfig); return true; } } diff --git a/src/modules/component.ts b/src/modules/component.ts index b16636a0..94172233 100644 --- a/src/modules/component.ts +++ b/src/modules/component.ts @@ -94,7 +94,7 @@ export class ComponentConfig { id: string; // component-wide variable configuration - variables?: Map; + variables: Map = new Map(); constructor(id: string) { this.id = id; diff --git a/src/modules/exec.ts b/src/modules/exec.ts index f2965ac6..c859e6bd 100644 --- a/src/modules/exec.ts +++ b/src/modules/exec.ts @@ -17,7 +17,7 @@ import { exec } from 'node:child_process'; import { join, resolve } from 'node:path'; import { ExecSpec, ProgramSpec } from './component'; import { ProjectCache } from './project-cache'; -import { ProjectConfig } from './project-config'; +import { ProjectConfig } from './projectConfig/projectConfig'; import { stdOutParser } from './stdout-parser'; const lineCapturer = (projectCache: ProjectCache, writeStdout: boolean, data: string) => { diff --git a/src/modules/package-downloader.ts b/src/modules/package-downloader.ts index c6d609f4..5f62533d 100644 --- a/src/modules/package-downloader.ts +++ b/src/modules/package-downloader.ts @@ -16,6 +16,7 @@ import { posix as pathPosix } from 'node:path'; import { CheckRepoActions, SimpleGit, simpleGit } from 'simple-git'; import { CliFileSystem } from '../utils/fs-bridge'; import { PackageConfig } from './package'; +import { BRANCH_PREFIX } from './semver'; export class PackageDownloader { packageConfig: PackageConfig; @@ -40,7 +41,11 @@ export class PackageDownloader { } async checkoutVersion(): Promise { - await this.git.checkout(this.packageConfig.version); + await this.git.checkout( + this.packageConfig.version.startsWith(BRANCH_PREFIX) + ? this.packageConfig.version.substring(BRANCH_PREFIX.length) + : this.packageConfig.version, + ); } async downloadPackage(option: { checkVersionOnly: boolean }): Promise { diff --git a/src/modules/package.ts b/src/modules/package.ts index bba8a1eb..8a62a162 100644 --- a/src/modules/package.ts +++ b/src/modules/package.ts @@ -27,37 +27,30 @@ export interface PackageManifest { export interface PackageConfigAttributes { repo: string; - - // @deprecated: do not use anymore - name?: string; - version: string; - variables?: Map; } export class PackageConfig { // name of the package to the package repository - repo: string = ''; + repo: string; // version of the package to use - version: string = ''; + version: string; // package-wide variable configuration - variables?: Map; + variables: Map = new Map(); constructor(attributes: PackageConfigAttributes) { this.repo = attributes.repo; - if (attributes.name) { - this.repo = attributes.name; - } this.version = attributes.version; - this.variables = attributes.variables; + this.variables = attributes.variables ? attributes.variables : this.variables; } private _isCustomPackage(): boolean { return this.repo.endsWith('.git'); } + /** * Return the fully qualified URL to the package repository. * In case of Eclipse Velocitas repos which can be referenced by name only, diff --git a/src/modules/project-config.ts b/src/modules/projectConfig/projectConfig.ts similarity index 52% rename from src/modules/project-config.ts rename to src/modules/projectConfig/projectConfig.ts index e996365e..d4c6f22d 100644 --- a/src/modules/project-config.ts +++ b/src/modules/projectConfig/projectConfig.ts @@ -12,28 +12,18 @@ // // SPDX-License-Identifier: Apache-2.0 -import { PathLike } from 'node:fs'; -import { resolve } from 'node:path'; -import { cwd } from 'node:process'; -import { CliFileSystem } from '../utils/fs-bridge'; -import { DEFAULT_APP_MANIFEST_PATH } from './app-manifest'; -import { ComponentConfig, ComponentContext } from './component'; -import { mapReplacer } from './helpers'; -import { PackageConfig, PackageConfigAttributes } from './package'; -import { PackageIndex } from './package-index'; -import { getLatestVersion } from './semver'; -import { VariableCollection } from './variables'; - -export const DEFAULT_CONFIG_FILE_NAME = '.velocitas.json'; -export const DEFAULT_CONFIG_LOCKFILE_NAME = '.velocitas-lock.json'; -export const DEFAULT_CONFIG_FILE_PATH = resolve(cwd(), DEFAULT_CONFIG_FILE_NAME); -export const DEFAULT_CONFIG_LOCKFILE_PATH = resolve(cwd(), DEFAULT_CONFIG_LOCKFILE_NAME); - -export interface ProjectConfigOptions { +import { DEFAULT_APP_MANIFEST_PATH } from '../app-manifest'; +import { ComponentConfig, ComponentContext } from '../component'; +import { PackageConfig } from '../package'; +import { PackageIndex } from '../package-index'; +import { getLatestVersion } from '../semver'; +import { VariableCollection } from '../variables'; + +export interface ProjectConfigAttributes { packages: PackageConfig[]; - components?: ComponentConfig[]; + components: ComponentConfig[]; variables: Map; - cliVersion?: string; + cliVersion: string; } export class ProjectConfig { @@ -49,65 +39,27 @@ export class ProjectConfig { // version of the CLI used by the project cliVersion: string; - private static _parsePackageConfig(packages: PackageConfig[]): PackageConfig[] { - const configArray: PackageConfig[] = []; - packages.forEach((packageConfig: PackageConfig) => { - configArray.push(new PackageConfig(packageConfig)); - }); - return configArray; - } /** * Create a new project configuration. * - * @param config The options to use when creating the confugration. May be undefined. + * @param cliVersion The version of the CLI used by the project. + * @param config The options to use when creating the configuration. May be undefined. */ - constructor(cliVersion: string, config?: ProjectConfigOptions) { - this._packages = config?.packages ? ProjectConfig._parsePackageConfig(config.packages) : this._packages; - this._components = config?.components ? config.components : this._components; - this._variables = config?.variables ? config.variables : this._variables; + constructor(cliVersion: string, config?: ProjectConfigAttributes) { + this._packages = config?.packages ? config.packages : []; + this._components = config?.components ? config.components : []; + this._variables = config?.variables ? config.variables : new Map(); this.cliVersion = config?.cliVersion ? config.cliVersion : cliVersion; } - static read(cliVersion: string, path: PathLike = DEFAULT_CONFIG_FILE_PATH, ignoreLock: boolean = false): ProjectConfig { - let config: ProjectConfig; - let projectConfigLock: ProjectConfigLock | null = null; - try { - config = new ProjectConfig(cliVersion, JSON.parse(CliFileSystem.readFileSync(path as string))); - } catch (error) { - throw new Error(`Error in parsing ${DEFAULT_CONFIG_FILE_NAME}: ${(error as Error).message}`); - } - - if (config._variables) { - config._variables = new Map(Object.entries(config._variables)); - } - - if (!ignoreLock && ProjectConfigLock.isAvailable()) { - projectConfigLock = ProjectConfigLock.read(); - } - - for (let packageConfig of config._packages) { - if (packageConfig.variables) { - packageConfig.variables = new Map(Object.entries(packageConfig.variables)); - } - if (projectConfigLock) { - packageConfig.version = projectConfigLock.findVersion(packageConfig.repo); - } - } - - if (config._components) { - for (let componentConfig of config._components) { - if (componentConfig.variables) { - componentConfig.variables = new Map(Object.entries(componentConfig.variables)); - } - } - } - - return config; - } - - static isAvailable = (path: PathLike = DEFAULT_CONFIG_FILE_PATH) => CliFileSystem.existsSync(path); - - static async create(usedComponents: Set, packageIndex: PackageIndex, cliVersion: string) { + /** + * Creates a new project configuration based on the provided components, package index, and CLI version. + * @param usedComponents A set of component IDs used in the project. + * @param packageIndex A package index object. + * @param cliVersion The version of the CLI. + * @returns A created ProjectConfig instance. + */ + static async create(usedComponents: Set, packageIndex: PackageIndex, cliVersion: string): Promise { const projectConfig = new ProjectConfig(`v${cliVersion}`); const usedPackageRepos = new Set(); for (const usedComponent of usedComponents) { @@ -127,35 +79,11 @@ export class ProjectConfig { } projectConfig.getVariableMappings().set('appManifestPath', DEFAULT_APP_MANIFEST_PATH); projectConfig.getVariableMappings().set('githubRepoId', ''); - projectConfig.write(); - } - - /** - * Write the project configuration to file. - * - * @param path Path of the file to write the configuration to. - */ - write(path: PathLike = DEFAULT_CONFIG_FILE_PATH): void { - // if we find an "old" project configuration with no components explicitly mentioned - // we persist all components we can find. - let componentsToSerialize: ComponentConfig[] = this._components; - - if (!componentsToSerialize || componentsToSerialize.length === 0) { - componentsToSerialize = this.getComponents(false, true).map((cc) => cc.config); - } - - const projectConfigOptions: ProjectConfigOptions = { - packages: this._packages, - components: componentsToSerialize, - variables: this._variables, - cliVersion: this.cliVersion, - }; - const configString = `${JSON.stringify(projectConfigOptions, mapReplacer, 4)}\n`; - CliFileSystem.writeFileSync(path, configString); + return projectConfig; } /** - * Return the configuration of a component. + * Returns the configuration of a component. * * @param componentId The ID of the component. * @returns The configuration of the component. @@ -169,64 +97,7 @@ export class ProjectConfig { } /** - * @param onlyInstalled only retrieves the installed packages for the project. Defaults to false. - * @returns all used packages by the project. - */ - getPackages(onlyInstalled: boolean = false): PackageConfig[] { - if (onlyInstalled) { - return this._packages.filter((pkg) => pkg.isPackageInstalled()); - } - - return this._packages; - } - - /** - * Searches through all / only the installed packageConfigs for the packageConfig with the specified - * name and returns it. If no packageConfig is found undefined is returned. - * @param packageName the packageName of the packageConfig to retrieve for. - * @param onlyInstalled true if searching only in the installed packages or false if all packages should be searched through. - * @returns the found packageConfig or undefined if none could be found. - */ - getPackageConfig(packageName: string, onlyInstalled: boolean = false): PackageConfig | undefined { - return this.getPackages(onlyInstalled).find((config) => config.getPackageName() === packageName); - } - - /** - * Adds a new packageConfig to the project. This method won't add a new package if a package with the - * same name already exists. Different versions are not taken into consideration. If updating the - * version of a packageConfig is required use #updatePackageConfig. - * - * @param packageConfig the packageConfig to add. - * @returns true if the package was added successfully, false otherwise. - */ - addPackageConfig(packageConfig: PackageConfig): boolean { - const existingPackage = this.getPackageConfig(packageConfig.getPackageName()); - - if (!existingPackage) { - this._packages.push(packageConfig); - return true; - } - return false; - } - - /** - * Updates the version of the packageConfig with the same packageName as provided. - * - * @param packageConfig the updated packageConfig. - * @returns true if the packageVersion was successfully updated, false otherwise. - */ - updatePackageConfig(packageConfig: PackageConfig): boolean { - const existingPackage = this.getPackageConfig(packageConfig.getPackageName()); - - if (existingPackage && existingPackage.version !== packageConfig.version) { - existingPackage?.setPackageVersion(packageConfig.version); - return true; - } - return false; - } - - /** - * Add a component from a referenced package to the project. + * Adds a component from a referenced package to the project. * * @param id ID of the component to add to the project. */ @@ -235,7 +106,7 @@ export class ProjectConfig { } /** - * Remove a used component from the project. + * Removes a used component from the project. * * @param id ID of the component to remove from the project. */ @@ -244,19 +115,18 @@ export class ProjectConfig { } /** - * Return all components used by the project. If the project specifies no components explicitly, + * Returns the contexts of all components used by the project. If the project specifies no components explicitly, * all components are used by default. * * @param onlyUsed Only include components used by the project. Default: true. - * @param onlyInstalled Only include components from packages which are installed. Default: false. * @returns A list of all components used by the project. */ - getComponents(onlyUsed: boolean = true, onlyInstalled: boolean = false): ComponentContext[] { + getComponentContexts(onlyUsed: boolean = true, onlyInstalled: boolean = false): ComponentContext[] { const componentContexts: ComponentContext[] = []; let packageConfigs = this.getPackages(onlyInstalled); for (const packageConfig of packageConfigs) { - const components = this.getComponentsForPackageConfig(packageConfig, onlyUsed); + const components = this.getComponentContextsForPackageConfig(packageConfig, onlyUsed); componentContexts.push(...components); } @@ -271,7 +141,7 @@ export class ProjectConfig { * @param onlyUsed Only include components used by the project. Default: true. * @returns A list of all components used by this particular packageConfig. */ - getComponentsForPackageConfig(packageConfig: PackageConfig, onlyUsed: boolean = true): ComponentContext[] { + getComponentContextsForPackageConfig(packageConfig: PackageConfig, onlyUsed: boolean = true): ComponentContext[] { const componentContexts: ComponentContext[] = []; const usedComponents = this._components; @@ -296,8 +166,11 @@ export class ProjectConfig { return componentContexts; } - validateUsedComponents() { - // Check for components in usedComponents that couldn't be found in any componentManifest + /** + * Validates the used components by checking if each component is found in any package manifest. + * Throws an error if a component is not found in any package manifest. + */ + validateUsedComponents(): void { this._components.forEach((compCfg: ComponentConfig) => { const foundInManifest = this.getPackages(true).some((packageConfig) => packageConfig.readPackageManifest().components.some((componentManifest) => componentManifest.id === compCfg.id), @@ -309,12 +182,12 @@ export class ProjectConfig { } /** - * Find a single component by its ID. + * Finds a single component by its ID. * @param componentId The component ID to find. * @returns The context the component is used in. */ findComponentByName(componentId: string): ComponentContext { - let result = this.getComponents(undefined, true).find((compCtx: ComponentContext) => compCtx.manifest.id === componentId); + let result = this.getComponentContexts(undefined, true).find((compCtx: ComponentContext) => compCtx.manifest.id === componentId); if (!result) { throw Error(`Cannot find component with id '${componentId}'!`); @@ -324,80 +197,82 @@ export class ProjectConfig { } /** - * @returns all declared variable mappings on project level. + * @param onlyInstalled only retrieves the installed packages for the project. Defaults to false. + * @returns all used packages by the project. */ - getVariableMappings(): Map { - return this._variables; - } + getPackages(onlyInstalled: boolean = false): PackageConfig[] { + if (onlyInstalled) { + return this._packages.filter((pkg) => pkg.isPackageInstalled()); + } - getVariableCollection(currentComponentContext: ComponentContext): VariableCollection { - return VariableCollection.build(this.getComponents(undefined, true), this.getVariableMappings(), currentComponentContext); + return this._packages; } -} - -export class ProjectConfigLock { - packages: PackageConfig[]; - constructor(packages: PackageConfig[]) { - this.packages = packages; + /** + * Searches through all / only the installed packageConfigs for the packageConfig with the specified + * name and returns it. If no packageConfig is found undefined is returned. + * @param packageName the packageName of the packageConfig to retrieve for. + * @param onlyInstalled true if searching only in the installed packages or false if all packages should be searched through. + * @returns the found packageConfig or undefined if none could be found. + */ + getPackageConfig(packageName: string, onlyInstalled: boolean = false): PackageConfig | undefined { + return this.getPackages(onlyInstalled).find((config) => config.getPackageName() === packageName); } - static isAvailable = (path: PathLike = DEFAULT_CONFIG_LOCKFILE_PATH) => CliFileSystem.existsSync(path); - /** - * Reads the locked project configuration from file. - * @param path The path to the lock file. Defaults to DEFAULT_CONFIG_LOCKFILE_PATH if not provided. - * @returns An instance of ProjectConfigLock if the lock file exists and is readable, or null if the file is not present. - * @throws Error if there's an issue reading the lock file other than it not being present. + * Adds a new packageConfig to the project. This method won't add a new package if a package with the + * same name already exists. Different versions are not taken into consideration. If updating the + * version of a packageConfig is required use #updatePackageConfig. + * + * @param packageConfig the packageConfig to add. + * @returns true if the package was added successfully, false otherwise. */ - static read(path: PathLike = DEFAULT_CONFIG_LOCKFILE_PATH): ProjectConfigLock | null { - try { - const data = JSON.parse(CliFileSystem.readFileSync(path as string)); - const packages = data.packages; - return new ProjectConfigLock(packages); - } catch (error: any) { - if (error.code === 'ENOENT') { - return null; - } else { - throw new Error(`Error reading lock file: ${error.message}`); - } + addPackageConfig(packageConfig: PackageConfig): boolean { + const existingPackage = this.getPackageConfig(packageConfig.getPackageName()); + + if (!existingPackage) { + this._packages.push(packageConfig); + return true; } + return false; } /** - * Writes the locked project configuration to file. - * @param projectConfig Project configuration to get the packages for the lock file. - * @param path Path of the file to write the configuration to. Defaults to DEFAULT_CONFIG_LOCKFILE_PATH. + * Updates the version of the packageConfig with the same packageName as provided. + * + * @param packageConfig the updated packageConfig. + * @returns true if the packageVersion was successfully updated, false otherwise. */ - static write(projectConfig: ProjectConfig, path: PathLike = DEFAULT_CONFIG_LOCKFILE_PATH): void { - try { - const projectConfigOptions = { - packages: projectConfig.getPackages(), - }; - const configString = JSON.stringify(projectConfigOptions, null, 4); - CliFileSystem.writeFileSync(path, configString); - } catch (error) { - throw new Error(`Error writing .velocitas-lock.json: ${error}`); + updatePackageConfig(packageConfig: PackageConfig): boolean { + const existingPackage = this.getPackageConfig(packageConfig.getPackageName()); + + if (existingPackage && existingPackage.version !== packageConfig.version) { + existingPackage?.setPackageVersion(packageConfig.version); + return true; } + return false; } /** - * Finds the version of the specified package from the lock file. - * @param packageName Name of the package to find the version for. - * @returns The version of the specified package if found. - * @throws Error if the lock file is corrupted, the package is not found, or no version is stored for the package. + * @returns all used components by the project. */ - public findVersion(packageName: string): string { - const packageConfig = this.packages.find((pkg: PackageConfigAttributes) => pkg.repo === packageName); - - if (!packageConfig) { - throw new Error(`Package '${packageName}' not found in lock file.`); - } + getComponents(): ComponentConfig[] { + return this._components; + } - if (!packageConfig.version) { - throw new Error(`No version found for package '${packageName}' in lock file.`); - } + /** + * @returns all declared variable mappings on project level. + */ + getVariableMappings(): Map { + return this._variables; + } - return packageConfig.version; + /** + * Gets a variable collection for the specified component context. + * @param componentContext The context of the component for which the variable collection is obtained. + * @returns A variable collection containing variables from the project configuration and component context. + */ + getVariableCollection(currentComponentContext: ComponentContext): VariableCollection { + return VariableCollection.build(this.getComponentContexts(undefined, true), this.getVariableMappings(), currentComponentContext); } } diff --git a/src/modules/projectConfig/projectConfigConstants.ts b/src/modules/projectConfig/projectConfigConstants.ts new file mode 100644 index 00000000..cae5ed3a --- /dev/null +++ b/src/modules/projectConfig/projectConfigConstants.ts @@ -0,0 +1,32 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { resolve } from 'node:path'; +import { cwd } from 'node:process'; + +export type DesiredConfigFilePackages = { + [name: string]: string; +}; +export type DesiredConfigFileComponents = string[]; +export type DesiredConfigFileVariables = { + [name: string]: any; +}; + +const DEFAULT_CONFIG_FILE_NAME = '.velocitas.json'; +export const DEFAULT_CONFIG_FILE_PATH = resolve(cwd(), DEFAULT_CONFIG_FILE_NAME); + +const DEFAULT_CONFIG_LOCKFILE_NAME = '.velocitas-lock.json'; +export const DEFAULT_CONFIG_LOCKFILE_PATH = resolve(cwd(), DEFAULT_CONFIG_LOCKFILE_NAME); + +export const VARIABLE_SCOPE_SEPARATOR = '@'; diff --git a/src/modules/projectConfig/projectConfigFileReader.ts b/src/modules/projectConfig/projectConfigFileReader.ts new file mode 100644 index 00000000..e750bd02 --- /dev/null +++ b/src/modules/projectConfig/projectConfigFileReader.ts @@ -0,0 +1,278 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { PathLike } from 'node:fs'; +import { CliFileSystem } from '../../utils/fs-bridge'; +import { ComponentConfig } from '../component'; +import { PackageConfig } from '../package'; +import { ProjectConfig, ProjectConfigAttributes } from './projectConfig'; +import { + DEFAULT_CONFIG_FILE_PATH, + DEFAULT_CONFIG_LOCKFILE_PATH, + DesiredConfigFileComponents, + DesiredConfigFilePackages, + VARIABLE_SCOPE_SEPARATOR, +} from './projectConfigConstants'; +import { ProjectConfigLock } from './projectConfigLock'; +import { ReaderUtil } from './readerUtil'; + +interface IProjectConfigReader { + read(cliVersion: string, path: PathLike, ignoreLock: boolean): ProjectConfig; +} + +export class MultiFormatConfigReader implements IProjectConfigReader { + /** + * Reads the project configuration using readers for multiple formats. + * @param cliVersion The version of the CLI. + * @param path The path to the configuration file. + * @param ignoreLock Whether to ignore the project configuration lock file or not. + * @returns The project configuration. + * @throws Error if unable to read the configuration file in any format. + */ + read(cliVersion: string, path: PathLike = DEFAULT_CONFIG_FILE_PATH, ignoreLock: boolean = false): ProjectConfig { + const projectConfigReaders: IProjectConfigReader[] = [new ProjectConfigReader(), new LegacyProjectConfigReader()]; + + let config: ProjectConfig | null = null; + + for (const reader of projectConfigReaders) { + try { + config = reader.read(cliVersion, path, ignoreLock); + if (config !== null) { + break; + } + } catch (error: any) { + console.warn(`Warning: ${path} not in expected format: ${error.message}, falling back to legacy format reading.`); + } + } + + if (config === null) { + throw new Error(`Unable to read ${path}: unknown format!`); + } + + return config; + } +} + +/** + * Reader for .velocitas.json files. + */ +export class ProjectConfigReader implements IProjectConfigReader { + packages: PackageConfig[] = []; + components: ComponentConfig[] = []; + variables: Map = new Map(); + cliVersion: string = ''; + + /** + * Parses configuration variables and assigns them to the given configuration. + * @param configToAssign Configuration to which the variables will be assigned. + */ + private _assignVariablesToConfig(configToAssign: PackageConfig | ComponentConfig): void { + for (const [variableKey, variableValue] of this.variables) { + if ( + (configToAssign instanceof PackageConfig && variableKey.includes(configToAssign.repo)) || + (configToAssign instanceof ComponentConfig && variableKey.includes(configToAssign.id)) + ) { + const [parsedVariableKey] = variableKey.split(VARIABLE_SCOPE_SEPARATOR); + configToAssign.variables?.set(parsedVariableKey, variableValue); + } + } + } + + /** + * Validates the structure of the configuration file packages. + * @param configFilePackages The configuration file packages to validate. + * @returns True if the configuration file packages are valid, otherwise false. + */ + private _isValidConfigFilePackages(configFilePackages: DesiredConfigFilePackages): boolean { + if (typeof configFilePackages !== 'object' || configFilePackages === null) { + return false; + } + for (const key in configFilePackages) { + if (typeof key !== 'string' || typeof configFilePackages[key] !== 'string') { + return false; + } + } + return true; + } + + /** + * Validates the structure of the configuration file components. + * @param configFileComponents The configuration file components to validate. + * @returns True if the configuration file components are valid, otherwise false. + */ + private _isValidConfigFileComponents(configFileComponents: DesiredConfigFileComponents): boolean { + if (!Array.isArray(configFileComponents)) { + return false; + } + for (const component of configFileComponents) { + if (typeof component !== 'string') { + return false; + } + } + return true; + } + + /** + * Converts DesiredConfigFilePackages into an array of PackageConfig objects. + * @param configFilePackages Object containing repository names as keys and version strings as values. + * @param ignoreLock If true, ignores project configuration lock file. + * @returns An array of PackageConfig objects. + */ + private _parseConfigToPackageConfigArray(configFilePackages: DesiredConfigFilePackages, ignoreLock: boolean): PackageConfig[] { + const pkgCfgArray: PackageConfig[] = []; + if (!this._isValidConfigFilePackages(configFilePackages)) { + throw new Error('Config File Packages are not in the expected format'); + } + const packages = configFilePackages instanceof Map ? configFilePackages : new Map(Object.entries(configFilePackages)); + for (const [repoName, version] of packages) { + const pkgCfg = new PackageConfig({ repo: repoName, version: version }); + this._assignVariablesToConfig(pkgCfg); + pkgCfgArray.push(pkgCfg); + } + ReaderUtil.parseLockFileVersions(pkgCfgArray, ignoreLock); + return pkgCfgArray; + } + + /** + * Converts DesiredConfigFileComponents into an array of ComponentConfig objects. + * @param configFileComponents String array of component IDs. + * @returns An array of ComponentConfig objects. + */ + private _parseConfigToComponentConfigArray(configFileComponents: DesiredConfigFileComponents): ComponentConfig[] { + if (!configFileComponents) { + return []; + } + if (!this._isValidConfigFileComponents(configFileComponents)) { + throw new Error('Config File Components are not in the expected format'); + } + const cmpCfgArray: ComponentConfig[] = configFileComponents.map((component: string) => { + const cmpCfg = new ComponentConfig(component); + if (this.variables) { + this._assignVariablesToConfig(cmpCfg); + } + return cmpCfg; + }); + return cmpCfgArray; + } + + /** + * Reads the project configuration. + * @param cliVersion The version of the CLI. + * @param path The path to the configuration file. + * @param ignoreLock Whether to ignore the project configuration lock file or not. + * @returns The project configuration. + * @throws Error if unable to read the configuration file in any format. + */ + read(cliVersion: string, path: PathLike = DEFAULT_CONFIG_FILE_PATH, ignoreLock: boolean = false): ProjectConfig { + try { + const configFileData = JSON.parse(CliFileSystem.readFileSync(path as string)); + this.variables = ReaderUtil.convertConfigFileVariablesToMap(configFileData.variables); + this.packages = this._parseConfigToPackageConfigArray(configFileData.packages, ignoreLock); + this.components = this._parseConfigToComponentConfigArray(configFileData.components); + this.cliVersion = configFileData.cliVersion ? configFileData.cliVersion : cliVersion; + const config: ProjectConfigAttributes = { + packages: this.packages, + components: this.components, + variables: this.variables, + cliVersion: this.cliVersion, + }; + return new ProjectConfig(cliVersion, config); + } catch (error) { + throw error; + } + } +} + +/** + * Reader for legacy .velocitas.json files in old format. + */ +export class LegacyProjectConfigReader implements IProjectConfigReader { + /** + * Parses legacy package configurations into an array of PackageConfig objects. + * @param configFilePackages Array containing configuration for packages. + * @param ignoreLock If true, ignores project configuration lock file. + * @returns An array of PackageConfig objects. + */ + private _parseLegacyPackageConfig(configFilePackages: PackageConfig[], ignoreLock: boolean): PackageConfig[] { + const configArray: PackageConfig[] = []; + ReaderUtil.parseLockFileVersions(configFilePackages, ignoreLock); + configFilePackages.forEach((packageConfig: PackageConfig) => { + packageConfig.variables = ReaderUtil.convertConfigFileVariablesToMap(packageConfig.variables); + configArray.push(new PackageConfig(packageConfig)); + }); + return configArray; + } + + /** + * Parses legacy component configurations into an array of ComponentConfig objects. + * @param configFileComponents Array containing configuration for components. + * @returns An array of ComponentConfig objects. + */ + private _parseLegacyComponentConfig(configFileComponents: ComponentConfig[]): ComponentConfig[] { + const configArray: ComponentConfig[] = []; + if (!configFileComponents) { + return configArray; + } + configFileComponents.forEach((componentConfig: ComponentConfig) => { + const cmpCfg = new ComponentConfig(componentConfig.id); + cmpCfg.variables = ReaderUtil.convertConfigFileVariablesToMap(componentConfig.variables); + configArray.push(cmpCfg); + }); + return configArray; + } + + /** + * Reads the legacy project configuration in old format. + * @param cliVersion The version of the CLI. + * @param path The path to the configuration file. + * @param ignoreLock Whether to ignore the project configuration lock file or not. + * @returns The project configuration. + * @throws Error if unable to read the configuration file in any format. + */ + read(cliVersion: string, path: PathLike = DEFAULT_CONFIG_FILE_PATH, ignoreLock: boolean = false): ProjectConfig { + const configFileData = JSON.parse(CliFileSystem.readFileSync(path as string)); + const config: ProjectConfigAttributes = { + packages: this._parseLegacyPackageConfig(configFileData.packages, ignoreLock), + components: this._parseLegacyComponentConfig(configFileData.components), + variables: ReaderUtil.convertConfigFileVariablesToMap(configFileData.variables), + cliVersion: configFileData.cliVersion ? configFileData.cliVersion : cliVersion, + }; + return new ProjectConfig(cliVersion, config); + } +} + +/** + * Reader for .velocitas-lock.json files. + */ +export class ProjectConfigLockReader { + /** + * Reads the locked project configuration from file. + * @param path The path to the lock file. Defaults to DEFAULT_CONFIG_LOCKFILE_PATH if not provided. + * @returns An instance of ProjectConfigLock if the lock file exists and is readable, or null if the file is not present. + * @throws Error if there's an issue reading the lock file other than it not being present. + */ + read(path: PathLike = DEFAULT_CONFIG_LOCKFILE_PATH): ProjectConfigLock | null { + try { + const data = JSON.parse(CliFileSystem.readFileSync(path as string)); + const packages = new Map(Object.entries(data.packages)); + return new ProjectConfigLock(packages); + } catch (error: any) { + if (error.code === 'ENOENT') { + return null; + } else { + throw new Error(`Error reading lock file: ${error.message}`); + } + } + } +} diff --git a/src/modules/projectConfig/projectConfigFileWriter.ts b/src/modules/projectConfig/projectConfigFileWriter.ts new file mode 100644 index 00000000..a09e167c --- /dev/null +++ b/src/modules/projectConfig/projectConfigFileWriter.ts @@ -0,0 +1,106 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { PathLike } from 'node:fs'; +import { CliFileSystem } from '../../utils/fs-bridge'; +import { ComponentConfig } from '../component'; +import { mapReplacer } from '../helpers'; +import { PackageConfig } from '../package'; +import { ProjectConfig } from './projectConfig'; +import { DEFAULT_CONFIG_FILE_PATH, DEFAULT_CONFIG_LOCKFILE_PATH } from './projectConfigConstants'; + +interface IProjectConfigWriter { + write(projectConfig: ProjectConfig, path: PathLike): void; +} + +/** + * Writer for .velocitas.json files. + */ +export class ProjectConfigWriter implements IProjectConfigWriter { + /** + * Creates a string out of the project configuration in format for the lock file. + * @returns A string for writing the ProjectConfigLock. + */ + static toLockString(projectConfig: ProjectConfig): string { + const packagesObject: { [key: string]: string } = {}; + projectConfig.getPackages().forEach((packageConfig: PackageConfig) => { + packagesObject[`${packageConfig.repo}`] = packageConfig.version; + }); + const projectConfigAttributes = { + packages: packagesObject, + }; + + return `${JSON.stringify(projectConfigAttributes, null, 4)}\n`; + } + + /** + * Converts an array of PackageConfig objects into a writable Map for the configuration file. + * @param packageConfig Array of PackageConfig objects. + * @returns A Map containing repository names as keys and version identifiers as values. + */ + private _toWritablePackageConfig(packageConfig: PackageConfig[]): Map { + return new Map(packageConfig.map((pkg: PackageConfig) => [pkg.repo, pkg.version])); + } + + /** + * Converts an array of ComponentConfig objects into a writable string array for the configuration file. + * @param componentConfig Array of ComponentConfig objects. + * @returns A string array containing the IDs of the components. + */ + private _toWritableComponentConfig(componentConfig: ComponentConfig[]): string[] { + return Array.from(new Set(componentConfig.map((component: ComponentConfig) => component.id))); + } + + /** + * Write the project configuration to file. + * @param projectConfig Project configuration object to write to a file. + * @param path Path of the file to write the configuration to. + */ + write(projectConfig: ProjectConfig, path: PathLike = DEFAULT_CONFIG_FILE_PATH): void { + // if we find an "old" project configuration with no components explicitly mentioned + // we persist all components we can find. + let componentsToSerialize: ComponentConfig[] = projectConfig.getComponents(); + + if (!componentsToSerialize || componentsToSerialize.length === 0) { + componentsToSerialize = projectConfig.getComponentContexts(false, true).map((cc) => cc.config); + } + + const projectConfigAttributes = { + packages: this._toWritablePackageConfig(projectConfig.getPackages()), + components: this._toWritableComponentConfig(componentsToSerialize), + variables: projectConfig.getVariableMappings(), + cliVersion: projectConfig.cliVersion, + }; + const configString = `${JSON.stringify(projectConfigAttributes, mapReplacer, 4)}\n`; + CliFileSystem.writeFileSync(path, configString); + } +} + +/** + * Writer for .velocitas-lock.json files. + */ +export class ProjectConfigLockWriter implements IProjectConfigWriter { + /** + * Writes the locked project configuration to file. + * @param projectConfig Project configuration to get the packages for the lock file. + * @param path Path of the file to write the configuration to. Defaults to DEFAULT_CONFIG_LOCKFILE_PATH. + */ + write(projectConfig: ProjectConfig, path: PathLike = DEFAULT_CONFIG_LOCKFILE_PATH): void { + try { + CliFileSystem.writeFileSync(path, ProjectConfigWriter.toLockString(projectConfig)); + } catch (error) { + throw new Error(`Error writing .velocitas-lock.json: ${error}`); + } + } +} diff --git a/src/modules/projectConfig/projectConfigIO.ts b/src/modules/projectConfig/projectConfigIO.ts new file mode 100644 index 00000000..01aa0e54 --- /dev/null +++ b/src/modules/projectConfig/projectConfigIO.ts @@ -0,0 +1,77 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { PathLike } from 'node:fs'; +import { CliFileSystem } from '../../utils/fs-bridge'; +import { ProjectConfig } from './projectConfig'; +import { DEFAULT_CONFIG_FILE_PATH, DEFAULT_CONFIG_LOCKFILE_PATH } from './projectConfigConstants'; +import { MultiFormatConfigReader, ProjectConfigLockReader } from './projectConfigFileReader'; +import { ProjectConfigLockWriter, ProjectConfigWriter } from './projectConfigFileWriter'; +import { ProjectConfigLock } from './projectConfigLock'; + +export class ProjectConfigIO { + /** + * Checks if a configuration file exists at the given path. + * @param path The path to the configuration file. + * @returns True if the configuration file exists, otherwise false. + */ + static isConfigAvailable = (path: PathLike = DEFAULT_CONFIG_FILE_PATH): boolean => CliFileSystem.existsSync(path); + + /** + * Reads the project configuration using readers for multiple formats. + * @param cliVersion The version of the CLI. + * @param path The path to the configuration file. + * @param ignoreLock Whether to ignore the project configuration lock file or not. + * @returns The project configuration. + * @throws Error if unable to read the configuration file in any format. + */ + static read(cliVersion: string, path: PathLike = DEFAULT_CONFIG_FILE_PATH, ignoreLock: boolean = false): ProjectConfig { + return new MultiFormatConfigReader().read(cliVersion, path, ignoreLock); + } + + /** + * Write the project configuration to file. + * @param projectConfig Project configuration object to write to a file. + * @param path Path of the file to write the configuration to. + */ + static write(config: ProjectConfig, path: PathLike = DEFAULT_CONFIG_FILE_PATH): void { + new ProjectConfigWriter().write(config, path); + } + + /** + * Checks if a locked configuration file exists at the given path. + * @param path The path to the configuration file. + * @returns True if the configuration file exists, otherwise false. + */ + static isLockAvailable = (path: PathLike = DEFAULT_CONFIG_LOCKFILE_PATH): boolean => CliFileSystem.existsSync(path); + + /** + * Reads the locked project configuration from file. + * @param path The path to the lock file. Defaults to DEFAULT_CONFIG_LOCKFILE_PATH if not provided. + * @returns An instance of ProjectConfigLock if the lock file exists and is readable, or null if the file is not present. + * @throws Error if there's an issue reading the lock file other than it not being present. + */ + static readLock(path: PathLike = DEFAULT_CONFIG_LOCKFILE_PATH): ProjectConfigLock | null { + return new ProjectConfigLockReader().read(path); + } + + /** + * Writes the locked project configuration to file. + * @param projectConfig Project configuration to get the packages for the lock file. + * @param path Path of the file to write the configuration to. Defaults to DEFAULT_CONFIG_LOCKFILE_PATH. + */ + static writeLock(projectConfig: ProjectConfig, path: PathLike = DEFAULT_CONFIG_LOCKFILE_PATH): void { + new ProjectConfigLockWriter().write(projectConfig, path); + } +} diff --git a/src/modules/projectConfig/projectConfigLock.ts b/src/modules/projectConfig/projectConfigLock.ts new file mode 100644 index 00000000..a70854dc --- /dev/null +++ b/src/modules/projectConfig/projectConfigLock.ts @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +interface ProjectConfigLockAttributes { + packages: Map; +} + +export class ProjectConfigLock implements ProjectConfigLockAttributes { + packages: Map; + + constructor(packages: Map) { + this.packages = packages; + } + + /** + * Finds the version of the specified package from the lock file. + * @param packageName Name of the package to find the version for. + * @returns The version of the specified package if found. + * @throws Error if no package version is found. + */ + public findVersion(packageName: string): string { + const packageVersion = this.packages.get(packageName); + if (!packageVersion) { + throw new Error(`Package '${packageName}' not found in lock file.`); + } + return packageVersion; + } +} diff --git a/src/modules/projectConfig/readerUtil.ts b/src/modules/projectConfig/readerUtil.ts new file mode 100644 index 00000000..ba4023d4 --- /dev/null +++ b/src/modules/projectConfig/readerUtil.ts @@ -0,0 +1,53 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { PackageConfig } from '../package'; +import { DesiredConfigFileVariables } from './projectConfigConstants'; +import { ProjectConfigIO } from './projectConfigIO'; +import { ProjectConfigLock } from './projectConfigLock'; + +/** + * Utility functions for reading project configuration files. + */ +export namespace ReaderUtil { + /** + * Converts DesiredConfigFileVariables into a Map format. + * @param configFileVariables Configuration file variables to convert. + * @returns A Map representing the configuration file variables. + */ + export function convertConfigFileVariablesToMap(configFileVariables: DesiredConfigFileVariables): Map { + if (configFileVariables && !(configFileVariables instanceof Map)) { + return new Map(Object.entries(configFileVariables)); + } + return new Map(); + } + + /** + * Processes all packageConfigs to read versions from lock file. + * @param packageConfigs Package Configurations to process. + * @param ignoreLock If true, ignores project configuration lock file. + */ + export function parseLockFileVersions(packageConfigs: PackageConfig[], ignoreLock: boolean): PackageConfig[] { + let projectConfigLock: ProjectConfigLock | null = null; + if (!ignoreLock && ProjectConfigIO.isLockAvailable()) { + projectConfigLock = ProjectConfigIO.readLock(); + } + if (projectConfigLock) { + for (let packageConfig of packageConfigs) { + packageConfig.version = projectConfigLock.findVersion(packageConfig.repo); + } + } + return packageConfigs; + } +} diff --git a/src/modules/semver.ts b/src/modules/semver.ts index b50ae3dd..11ce2244 100644 --- a/src/modules/semver.ts +++ b/src/modules/semver.ts @@ -15,6 +15,8 @@ import { SemVer, gt, maxSatisfying, satisfies, valid } from 'semver'; import { TagResult } from 'simple-git'; +export const BRANCH_PREFIX = '@'; + export function getLatestVersion(versions: string[]): string { let latestVersion: SemVer | undefined = undefined; for (const version of versions) { @@ -40,9 +42,8 @@ export function getLatestVersion(versions: string[]): string { } export function resolveVersionIdentifier(versions: TagResult, versionIdentifier: string): string { - const branchPrefix = '@'; - if (versionIdentifier.startsWith(branchPrefix)) { - return versionIdentifier.substring(1); + if (versionIdentifier.startsWith(BRANCH_PREFIX)) { + return versionIdentifier; } if (versionIdentifier === 'latest') { @@ -53,7 +54,7 @@ export function resolveVersionIdentifier(versions: TagResult, versionIdentifier: if (matchedVersion === null) { throw new Error( - `Can't find matching version for ${versionIdentifier}. Prefix with '${branchPrefix}' for a branch or use a valid semantic version.`, + `Can't find matching version for ${versionIdentifier}. Prefix with '${BRANCH_PREFIX}' for a branch or use a valid semantic version.`, ); } diff --git a/test/commands/component/component-add.test.ts b/test/commands/component/component-add.test.ts index 7130da55..e97011b7 100644 --- a/test/commands/component/component-add.test.ts +++ b/test/commands/component/component-add.test.ts @@ -13,7 +13,7 @@ // SPDX-License-Identifier: Apache-2.0 import { expect, test } from '@oclif/test'; -import { ProjectConfig } from '../../../src/modules/project-config'; +import { ProjectConfigIO } from '../../../src/modules/projectConfig/projectConfigIO'; import { mockFolders } from '../../utils/mockfs'; describe('component add', () => { @@ -24,8 +24,8 @@ describe('component add', () => { .command(['component add', 'unused-component']) .it('adds an unused component', (ctx) => { expect( - ProjectConfig.read('') - .getComponents() + ProjectConfigIO.read('') + .getComponentContexts() .map((componentCtx) => componentCtx.manifest.id), ).to.contain('unused-component'); }); diff --git a/test/commands/component/component-remove.test.ts b/test/commands/component/component-remove.test.ts index 9021f637..9e995ffc 100644 --- a/test/commands/component/component-remove.test.ts +++ b/test/commands/component/component-remove.test.ts @@ -13,7 +13,7 @@ // SPDX-License-Identifier: Apache-2.0 import { expect, test } from '@oclif/test'; -import { ProjectConfig } from '../../../src/modules/project-config'; +import { ProjectConfigIO } from '../../../src/modules/projectConfig/projectConfigIO'; import { mockFolders } from '../../utils/mockfs'; describe('component remove', () => { @@ -24,8 +24,8 @@ describe('component remove', () => { .command(['component remove', 'test-runtime-local']) .it('removes a used component', (ctx) => { expect( - ProjectConfig.read('') - .getComponents() + ProjectConfigIO.read('') + .getComponentContexts() .map((componentCtx) => componentCtx.manifest.id), ).to.not.contain('test-runtime-local'); }); diff --git a/test/commands/create/create.test.ts b/test/commands/create/create.test.ts index b2ca1c2a..5a482488 100644 --- a/test/commands/create/create.test.ts +++ b/test/commands/create/create.test.ts @@ -17,10 +17,11 @@ import * as gitModule from 'simple-git'; import { AppManifest } from '../../../src/modules/app-manifest'; import * as exec from '../../../src/modules/exec'; import { CoreComponent, ExtensionComponent } from '../../../src/modules/package-index'; -import { ProjectConfig } from '../../../src/modules/project-config'; +import { ProjectConfigIO } from '../../../src/modules/projectConfig/projectConfigIO'; import { simpleGitInstanceMock } from '../../helpers/simpleGit'; -import { packageIndexMock } from '../../utils/mockConfig'; -import { installedCorePackage, installedRuntimePackage, mockFolders } from '../../utils/mockfs'; +import { corePackageInfoMock, packageIndexMock, runtimePackageInfoMock } from '../../utils/mockConfig'; +import { mockFolders } from '../../utils/mockfs'; + const inquirer = require('inquirer'); const TEST_APP_NAME = 'TestApp'; @@ -39,12 +40,12 @@ const TEST_EXPOSED_INTERFACE_PARAMETER_NAME_2 = TEST_COMPONENT_EXTENSION.paramet const TEST_EXPOSED_INTERFACE_PARAMETER_DEFAULT_2 = TEST_COMPONENT_EXTENSION.parameters![1].default as string; const TEST_PACKAGE_URI = packageIndexMock[0].package; -const TEST_PACKAGE_NAME = installedRuntimePackage.repo; -const TEST_PACKAGE_VERSION = installedRuntimePackage.version; +const TEST_PACKAGE_NAME = runtimePackageInfoMock.repo; +const TEST_PACKAGE_VERSION = runtimePackageInfoMock.resolvedVersion; const TEST_MAIN_PACKAGE_URI = packageIndexMock[1].package; -const TEST_MAIN_PACKAGE_NAME = installedCorePackage.repo; -const TEST_MAIN_PACKAGE_VERSION = installedCorePackage.version; +const TEST_MAIN_PACKAGE_NAME = corePackageInfoMock.repo; +const TEST_MAIN_PACKAGE_VERSION = corePackageInfoMock.resolvedVersion; enum CoreOption { fromExample = 0, @@ -79,10 +80,9 @@ describe('create', () => { .command(['create', '-n', TEST_APP_NAME, '-c', TEST_COMPONENT_CORE_ID]) .it('creates a project with provided flags and generates .velocitas.json and AppManifest', (ctx) => { expect(ctx.stdout).to.equal(EXPECTED_NON_INTERACTIVE_STDOUT); - expect(ProjectConfig.isAvailable()).to.be.true; + expect(ProjectConfigIO.isConfigAvailable()).to.be.true; expect(AppManifest.read()).to.not.be.undefined; - - const velocitasConfig = ProjectConfig.read('v0.0.0'); + const velocitasConfig = ProjectConfigIO.read('v0.0.0'); expect(velocitasConfig.getPackages()[0].repo).to.be.equal(TEST_MAIN_PACKAGE_URI); expect(velocitasConfig.getPackages()[0].version).to.be.equal(TEST_MAIN_PACKAGE_VERSION); expect(velocitasConfig.getPackages()[1].repo).to.be.equal(TEST_PACKAGE_URI); @@ -149,10 +149,9 @@ describe('create', () => { 'creates a project in interactive mode without example and generates .velocitas.json and AppManifest without defaults', (ctx) => { expect(ctx.stdout).to.equal(EXPECTED_INTERACTIVE_STDOUT(TEST_APP_NAME, TEST_COMPONENT_EXTENSION_ID)); - expect(ProjectConfig.isAvailable()).to.be.true; + expect(ProjectConfigIO.isConfigAvailable()).to.be.true; expect(AppManifest.read()).to.not.be.undefined; - - const velocitasConfig = ProjectConfig.read('v0.0.0'); + const velocitasConfig = ProjectConfigIO.read('v0.0.0'); expect(velocitasConfig.getPackages()[0].repo).to.be.equal(TEST_MAIN_PACKAGE_URI); expect(velocitasConfig.getPackages()[0].version).to.be.equal(TEST_MAIN_PACKAGE_VERSION); expect(velocitasConfig.getPackages()[1].repo).to.be.equal(TEST_PACKAGE_URI); @@ -189,10 +188,9 @@ describe('create', () => { .command(['create']) .it('creates a project in interactive mode without example and generates .velocitas.json and AppManifest correctly', (ctx) => { expect(ctx.stdout).to.equal(EXPECTED_INTERACTIVE_STDOUT(TEST_APP_NAME)); - expect(ProjectConfig.isAvailable()).to.be.true; + expect(ProjectConfigIO.isConfigAvailable()).to.be.true; expect(AppManifest.read()).to.not.be.undefined; - - const velocitasConfig = ProjectConfig.read('v0.0.0'); + const velocitasConfig = ProjectConfigIO.read('v0.0.0'); expect(velocitasConfig.getPackages()[0].repo).to.be.equal(TEST_MAIN_PACKAGE_URI); expect(velocitasConfig.getPackages()[0].version).to.be.equal(TEST_MAIN_PACKAGE_VERSION); expect(velocitasConfig.getPackages()[1].repo).to.be.equal(TEST_PACKAGE_URI); @@ -229,10 +227,10 @@ describe('create', () => { .command(['create']) .it('creates a project in interactive mode with example and generates .velocitas.json and AppManifest correctly', (ctx) => { expect(ctx.stdout).to.equal(EXPECTED_INTERACTIVE_STDOUT(TEST_COMPONENT_CORE_EXAMPLE)); - expect(ProjectConfig.isAvailable()).to.be.true; + expect(ProjectConfigIO.isConfigAvailable()).to.be.true; expect(AppManifest.read()).to.not.be.undefined; - const velocitasConfig = ProjectConfig.read('v0.0.0'); + const velocitasConfig = ProjectConfigIO.read('v0.0.0'); expect(velocitasConfig.getPackages()[0].repo).to.be.equal(TEST_MAIN_PACKAGE_URI); expect(velocitasConfig.getPackages()[0].version).to.be.equal(TEST_MAIN_PACKAGE_VERSION); expect(velocitasConfig.getPackages()[1].repo).to.be.equal(TEST_PACKAGE_URI); diff --git a/test/commands/init/init.test.ts b/test/commands/init/init.test.ts index 03f33dc3..7f492949 100644 --- a/test/commands/init/init.test.ts +++ b/test/commands/init/init.test.ts @@ -15,11 +15,11 @@ import { expect, test } from '@oclif/test'; import * as gitModule from 'simple-git'; import * as exec from '../../../src/modules/exec'; -import { ProjectConfigLock } from '../../../src/modules/project-config'; +import { ProjectConfigIO } from '../../../src/modules/projectConfig/projectConfigIO'; import { CliFileSystem } from '../../../src/utils/fs-bridge'; import { simpleGitInstanceMock } from '../../helpers/simpleGit'; -import { velocitasConfigMock } from '../../utils/mockConfig'; -import { installedCorePackage, installedRuntimePackage, installedSetupPackage, mockFolders, userHomeDir } from '../../utils/mockfs'; +import { corePackageInfoMock, runtimePackageInfoMock, setupPackageInfoMock } from '../../utils/mockConfig'; +import { mockFolders, userHomeDir } from '../../utils/mockfs'; describe('init', () => { test.do(() => { @@ -31,19 +31,23 @@ describe('init', () => { .command(['init']) .it('downloads packages from preconfigured velocitas.json', (ctx) => { expect(ctx.stdout).to.contain('Initializing Velocitas packages ...'); - expect(ctx.stdout).to.contain(`... Downloading package: '${installedRuntimePackage.repo}:${installedRuntimePackage.version}'`); - expect(ctx.stdout).to.contain(`... Downloading package: '${installedSetupPackage.repo}:${installedRuntimePackage.version}'`); + expect(ctx.stdout).to.contain( + `... Downloading package: '${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion}'`, + ); + expect(ctx.stdout).to.contain( + `... Downloading package: '${setupPackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion}'`, + ); expect( CliFileSystem.existsSync( - `${userHomeDir}/.velocitas/packages/${installedRuntimePackage.repo}/${installedRuntimePackage.version}`, + `${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}`, ), ).to.be.true; expect( CliFileSystem.existsSync( - `${userHomeDir}/.velocitas/packages/${installedSetupPackage.repo}/${installedSetupPackage.version}`, + `${userHomeDir}/.velocitas/packages/${setupPackageInfoMock.repo}/${setupPackageInfoMock.resolvedVersion}`, ), ).to.be.true; - expect(ProjectConfigLock.isAvailable()).to.be.true; + expect(ProjectConfigIO.isLockAvailable()).to.be.true; }); test.do(() => { @@ -56,31 +60,35 @@ describe('init', () => { .it('skips downloading because package is already installed', (ctx) => { expect(ctx.stdout).to.contain('Initializing Velocitas packages ...'); expect(ctx.stdout).to.contain( - `... Resolved '${installedRuntimePackage.repo}:${velocitasConfigMock.packages[0].version}' to version: '${installedRuntimePackage.version}'`, + `... Resolved '${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.versionIdentifier}' to version: '${runtimePackageInfoMock.resolvedVersion}'`, + ); + expect(ctx.stdout).to.contain( + `... '${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion}' already installed.`, ); - expect(ctx.stdout).to.contain(`... '${installedRuntimePackage.repo}:${installedRuntimePackage.version}' already installed.`); expect(ctx.stdout).to.contain( - `... Resolved '${installedSetupPackage.repo}:${velocitasConfigMock.packages[1].version}' to version: '${installedSetupPackage.version}'`, + `... Resolved '${setupPackageInfoMock.repo}:${setupPackageInfoMock.versionIdentifier}' to version: '${setupPackageInfoMock.resolvedVersion}'`, ); - expect(ctx.stdout).to.contain(`... '${installedSetupPackage.repo}:${installedSetupPackage.version}' already installed.`); + expect(ctx.stdout).to.contain(`... '${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion}' already installed.`); expect(ctx.stdout).to.contain( - `... Resolved '${installedCorePackage.repo}:${velocitasConfigMock.packages[2].version}' to version: '${installedCorePackage.version}'`, + `... Resolved '${corePackageInfoMock.repo}:${corePackageInfoMock.versionIdentifier}' to version: '${corePackageInfoMock.resolvedVersion}'`, ); - expect(ctx.stdout).to.contain(`... '${installedCorePackage.repo}:${installedCorePackage.version}' already installed.`); + expect(ctx.stdout).to.contain(`... '${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion}' already installed.`); expect( CliFileSystem.existsSync( - `${userHomeDir}/.velocitas/packages/${installedRuntimePackage.repo}/${installedRuntimePackage.version}`, + `${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}`, ), ).to.be.true; expect( CliFileSystem.existsSync( - `${userHomeDir}/.velocitas/packages/${installedSetupPackage.repo}/${installedSetupPackage.version}`, + `${userHomeDir}/.velocitas/packages/${setupPackageInfoMock.repo}/${setupPackageInfoMock.resolvedVersion}`, ), ).to.be.true; expect( - CliFileSystem.existsSync(`${userHomeDir}/.velocitas/packages/${installedCorePackage.repo}/${installedCorePackage.version}`), + CliFileSystem.existsSync( + `${userHomeDir}/.velocitas/packages/${corePackageInfoMock.repo}/${corePackageInfoMock.resolvedVersion}`, + ), ).to.be.true; - expect(ProjectConfigLock.isAvailable()).to.be.true; + expect(ProjectConfigIO.isLockAvailable()).to.be.true; }); test.do(() => { @@ -105,7 +113,7 @@ describe('init', () => { '... Directory is no velocitas project. Creating .velocitas.json at the root of your repository.', ); expect(CliFileSystem.existsSync(`${process.cwd()}/.velocitas.json`)).to.be.true; - expect(ProjectConfigLock.isAvailable()).to.be.true; + expect(ProjectConfigIO.isLockAvailable()).to.be.true; }); test.do(() => { @@ -147,11 +155,13 @@ describe('init', () => { .stdout() .stub(gitModule, 'simpleGit', (stub) => stub.returns(simpleGitInstanceMock())) .stub(exec, 'runExecSpec', (stub) => stub.returns({})) - .command(['init', '--package', `${installedCorePackage.repo}`]) + .command(['init', '--package', `${corePackageInfoMock.repo}`]) .it('downloads correctly the latest package if no version is specified', (ctx) => { - expect(ctx.stdout).to.contain(`... Downloading package: '${installedCorePackage.repo}:${installedCorePackage.version}'`); + expect(ctx.stdout).to.contain(`... Downloading package: '${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion}'`); expect( - CliFileSystem.existsSync(`${userHomeDir}/.velocitas/packages/${installedCorePackage.repo}/${installedCorePackage.version}`), + CliFileSystem.existsSync( + `${userHomeDir}/.velocitas/packages/${corePackageInfoMock.repo}/${corePackageInfoMock.resolvedVersion}`, + ), ).to.be.true; }); @@ -161,11 +171,13 @@ describe('init', () => { .stdout() .stub(gitModule, 'simpleGit', (stub) => stub.returns(simpleGitInstanceMock())) .stub(exec, 'runExecSpec', (stub) => stub.returns({})) - .command(['init', '--package', `${installedCorePackage.repo}`, '--specifier', `${installedCorePackage.version}`]) + .command(['init', '--package', `${corePackageInfoMock.repo}`, '--specifier', `${corePackageInfoMock.versionIdentifier}`]) .it('correctly downloads the defined package if a version is specified', (ctx) => { - expect(ctx.stdout).to.contain(`... Downloading package: '${installedCorePackage.repo}:${installedCorePackage.version}'`); + expect(ctx.stdout).to.contain(`... Downloading package: '${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion}'`); expect( - CliFileSystem.existsSync(`${userHomeDir}/.velocitas/packages/${installedCorePackage.repo}/${installedCorePackage.version}`), + CliFileSystem.existsSync( + `${userHomeDir}/.velocitas/packages/${corePackageInfoMock.repo}/${corePackageInfoMock.resolvedVersion}`, + ), ).to.be.true; }); @@ -175,7 +187,7 @@ describe('init', () => { .stdout() .stub(gitModule, 'simpleGit', (stub) => stub.returns(simpleGitInstanceMock())) .stub(exec, 'runExecSpec', (stub) => stub.returns({})) - .command(['init', '--package', `${installedCorePackage.repo}`, '--specifier', 'v10.5.2']) + .command(['init', '--package', `${corePackageInfoMock.repo}`, '--specifier', 'v10.5.2']) .catch((err) => { expect(err.message).to.contain(`Can't find matching version for v10.5.2.`); }) diff --git a/test/commands/package/package.test.ts b/test/commands/package/package.test.ts index 19ef1e18..7d630d60 100644 --- a/test/commands/package/package.test.ts +++ b/test/commands/package/package.test.ts @@ -13,7 +13,8 @@ // SPDX-License-Identifier: Apache-2.0 import { expect, test } from '@oclif/test'; -import { installedRuntimePackage, mockFolders, userHomeDir } from '../../utils/mockfs'; +import { runtimePackageInfoMock } from '../../utils/mockConfig'; +import { mockFolders, userHomeDir } from '../../utils/mockfs'; describe('package', () => { test.do(() => { @@ -30,10 +31,10 @@ describe('package', () => { mockFolders({ velocitasConfig: true, velocitasConfigLock: true, installedComponents: true }); }) .stdout() - .command(['package', '-p', `${installedRuntimePackage.repo}`]) + .command(['package', '-p', `${runtimePackageInfoMock.repo}`]) .it('prints the path of specified package', (ctx) => { expect(ctx.stdout).to.contain( - `${userHomeDir}/.velocitas/packages/${installedRuntimePackage.repo}/${installedRuntimePackage.version}`, + `${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}`, ); }); @@ -42,7 +43,7 @@ describe('package', () => { }) .stdout() .command(['package']) - .catch(`Cannot find package ${installedRuntimePackage.repo}:${installedRuntimePackage.version}`) + .catch(`Cannot find package ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion}`) .it('throws error when configured package cannot be found'); test.do(() => { diff --git a/test/commands/upgrade/upgrade.test.ts b/test/commands/upgrade/upgrade.test.ts index 34a4d65e..6529fa8e 100644 --- a/test/commands/upgrade/upgrade.test.ts +++ b/test/commands/upgrade/upgrade.test.ts @@ -18,7 +18,8 @@ import * as gitModule from 'simple-git'; import Init from '../../../src/commands/init'; import * as upgrade from '../../../src/commands/upgrade'; import { simpleGitInstanceMock } from '../../helpers/simpleGit'; -import { installedCorePackage, installedRuntimePackage, installedSetupPackage, mockFolders } from '../../utils/mockfs'; +import { corePackageInfoMock, runtimePackageInfoMock, setupPackageInfoMock } from '../../utils/mockConfig'; +import { mockFolders } from '../../utils/mockfs'; const mockedNewVersionTag = 'v2.0.0'; const mockedLowerVersionTag = 'v1.0.0'; @@ -50,9 +51,9 @@ describe('upgrade command', () => { .command(['upgrade']) .it('should report configured package version specifiers are up to date', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); - expect(ctx.stdout).to.contain(`... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedSetupPackage.repo}:${installedSetupPackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → up to date!`); + expect(ctx.stdout).to.contain(`... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → up to date!`); }); test.do(() => { @@ -63,9 +64,9 @@ describe('upgrade command', () => { .command(['upgrade']) .it('should report configured package version specifiers are up to date based on configured version range', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); - expect(ctx.stdout).to.contain(`... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedSetupPackage.repo}:${installedSetupPackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → up to date!`); + expect(ctx.stdout).to.contain(`... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → up to date!`); }); test.do(() => { @@ -77,10 +78,14 @@ describe('upgrade command', () => { .it('should upgrade configured package version specifiers based on configured version range', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); expect(ctx.stdout).to.contain( - `... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → ${mockedHigherVersionTag}`, + `... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → ${mockedHigherVersionTag}`, + ); + expect(ctx.stdout).to.contain( + `... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → ${mockedHigherVersionTag}`, + ); + expect(ctx.stdout).to.contain( + `... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → ${mockedHigherVersionTag}`, ); - expect(ctx.stdout).to.contain(`... ${installedSetupPackage.repo}:${installedSetupPackage.version} → ${mockedHigherVersionTag}`); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → ${mockedHigherVersionTag}`); expect(ctx.stdout).to.contain("Update available: Call 'velocitas init'"); }); @@ -94,12 +99,14 @@ describe('upgrade command', () => { .it('should upgrade configured package version specifiers to latest versions', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); expect(ctx.stdout).to.contain( - `... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → ${mockedNewVersionTag}`, + `... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, + ); + expect(ctx.stdout).to.contain( + `... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, ); expect(ctx.stdout).to.contain( - `... ${installedSetupPackage.repo}:${installedSetupPackage.version} → ${mockedNewVersionTag}`, + `... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, ); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → ${mockedNewVersionTag}`); expect(ctx.stdout).to.contain("Update available: Call 'velocitas init'"); }); @@ -119,9 +126,9 @@ describe('upgrade command', () => { .command(['upgrade', '--dry-run']) .it('should report configured package version specifiers are up to date', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); - expect(ctx.stdout).to.contain(`... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedSetupPackage.repo}:${installedSetupPackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → up to date!`); + expect(ctx.stdout).to.contain(`... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → up to date!`); }); test.do(() => { @@ -132,9 +139,9 @@ describe('upgrade command', () => { .command(['upgrade', '--dry-run']) .it('should report configured package version specifiers are up to date based on configured version range', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); - expect(ctx.stdout).to.contain(`... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedSetupPackage.repo}:${installedSetupPackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → up to date!`); + expect(ctx.stdout).to.contain(`... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → up to date!`); }); test.do(() => { @@ -146,13 +153,13 @@ describe('upgrade command', () => { .it('should report configured package version specifiers can be updated based on configured version range', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); expect(ctx.stdout).to.contain( - `... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → ${mockedHigherVersionTag}`, + `... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → ${mockedHigherVersionTag}`, ); expect(ctx.stdout).to.contain( - `... ${installedSetupPackage.repo}:${installedSetupPackage.version} → ${mockedHigherVersionTag}`, + `... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → ${mockedHigherVersionTag}`, ); expect(ctx.stdout).to.contain( - `... ${installedCorePackage.repo}:${installedCorePackage.version} → ${mockedHigherVersionTag}`, + `... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → ${mockedHigherVersionTag}`, ); }); @@ -164,9 +171,9 @@ describe('upgrade command', () => { .command(['upgrade', '--dry-run', '--ignore-bounds']) .it('should report configured package version specifiers are up to date according to all available versions', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); - expect(ctx.stdout).to.contain(`... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedSetupPackage.repo}:${installedSetupPackage.version} → up to date!`); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → up to date!`); + expect(ctx.stdout).to.contain(`... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → up to date!`); + expect(ctx.stdout).to.contain(`... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → up to date!`); }); test.do(() => { @@ -178,12 +185,14 @@ describe('upgrade command', () => { .it('should report configured package version specifiers can be updated according to all available versions', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); expect(ctx.stdout).to.contain( - `... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → ${mockedNewVersionTag}`, + `... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, + ); + expect(ctx.stdout).to.contain( + `... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, ); expect(ctx.stdout).to.contain( - `... ${installedSetupPackage.repo}:${installedSetupPackage.version} → ${mockedNewVersionTag}`, + `... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, ); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → ${mockedNewVersionTag}`); }); test.do(() => { mockFolders({ velocitasConfig: true, velocitasConfigLock: true }); @@ -195,12 +204,14 @@ describe('upgrade command', () => { .it('should upgrade configured package version specifiers and initialize new versions', (ctx) => { expect(ctx.stdout).to.contain('Checking .velocitas.json for updates!'); expect(ctx.stdout).to.contain( - `... ${installedRuntimePackage.repo}:${installedRuntimePackage.version} → ${mockedNewVersionTag}`, + `... ${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, + ); + expect(ctx.stdout).to.contain( + `... ${setupPackageInfoMock.repo}:${setupPackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, ); expect(ctx.stdout).to.contain( - `... ${installedSetupPackage.repo}:${installedSetupPackage.version} → ${mockedNewVersionTag}`, + `... ${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion} → ${mockedNewVersionTag}`, ); - expect(ctx.stdout).to.contain(`... ${installedCorePackage.repo}:${installedCorePackage.version} → ${mockedNewVersionTag}`); }); }); }); diff --git a/test/system-test/init.stest.ts b/test/system-test/init.stest.ts index c871ada3..c5f203c9 100644 --- a/test/system-test/init.stest.ts +++ b/test/system-test/init.stest.ts @@ -19,7 +19,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { DEFAULT_BUFFER_ENCODING } from '../../src/modules/constants'; import { ProjectCache } from '../../src/modules/project-cache'; -import { ProjectConfig, ProjectConfigLock } from '../../src/modules/project-config'; +import { ProjectConfigIO } from '../../src/modules/projectConfig/projectConfigIO'; import { TEST_ROOT, VELOCITAS_HOME, VELOCITAS_PROCESS } from '../utils/systemTestConfig'; const isDirectoryEmpty = (directoryPath: string): boolean => { @@ -44,8 +44,8 @@ describe('CLI command', () => { expect(initOutput.status).to.equal(0); const packageIndex = JSON.parse(readFileSync('./.velocitas.json', DEFAULT_BUFFER_ENCODING)); - const projectConfig = ProjectConfig.read(packageIndex.cliVersion, './.velocitas.json'); - const projectConfigLock = ProjectConfigLock.read('./.velocitas-lock.json'); + const projectConfig = ProjectConfigIO.read(packageIndex.cliVersion, './.velocitas.json'); + const projectConfigLock = ProjectConfigIO.readLock('./.velocitas-lock.json'); expect(projectConfigLock).to.not.be.null; expect(existsSync(join(ProjectCache.getCacheDir(), 'vehicle_model'))).to.be.true; @@ -56,14 +56,14 @@ describe('CLI command', () => { expect(isDirectoryEmpty(projectPackage.getPackageDirectoryWithVersion())).to.be.false; } }); - it('should be able to clean init a project with an older version of .velocitas.json', async () => { - copySync('./.velocitasOld.json', './.velocitas.json'); + it('should be able to clean init a project with a legacy version of .velocitas.json', async () => { + copySync('./.velocitasLegacy.json', './.velocitas.json'); const initOutput = spawnSync(VELOCITAS_PROCESS, ['init'], { encoding: DEFAULT_BUFFER_ENCODING }); expect(initOutput.status).to.equal(0); const packageIndex = JSON.parse(readFileSync('./.velocitas.json', DEFAULT_BUFFER_ENCODING)); - const projectConfig = ProjectConfig.read(packageIndex.cliVersion, './.velocitas.json'); - const projectConfigLock = ProjectConfigLock.read('./.velocitas-lock.json'); + const projectConfig = ProjectConfigIO.read(packageIndex.cliVersion, './.velocitas.json'); + const projectConfigLock = ProjectConfigIO.readLock('./.velocitas-lock.json'); expect(projectConfigLock).to.not.be.null; expect(existsSync('./gen')).to.be.true; diff --git a/test/system-test/sync.stest.ts b/test/system-test/sync.stest.ts new file mode 100644 index 00000000..90bb5f66 --- /dev/null +++ b/test/system-test/sync.stest.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from 'chai'; +import { copySync, removeSync } from 'fs-extra'; +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { DEFAULT_BUFFER_ENCODING } from '../../src/modules/constants'; +import { TEST_ROOT, VELOCITAS_HOME, VELOCITAS_PROCESS } from '../utils/systemTestConfig'; + +const packageManifestOne = JSON.parse( + readFileSync('./testbench/test-sync/packages/test-packageOne/test-version/manifest.json', DEFAULT_BUFFER_ENCODING), +); +const packageManifestTwo = JSON.parse( + readFileSync('./testbench/test-sync/packages/test-packageTwo/test-version/manifest.json', DEFAULT_BUFFER_ENCODING), +); + +const fileOneDestination = packageManifestOne.components[0].files[0].dst; +const fileTwoDestination = packageManifestTwo.components[0].files[0].dst; + +describe('CLI command', () => { + describe('sync', () => { + beforeEach(() => { + process.chdir(`${TEST_ROOT}/testbench/test-sync`); + copySync('./packages', `${VELOCITAS_HOME}/packages`); + }); + afterEach(() => { + removeSync(`./${fileOneDestination}`); + removeSync(`./${fileTwoDestination}`); + }); + it('should sync configured setup components and replace variables accordingly', async () => { + const syncOutput = spawnSync(VELOCITAS_PROCESS, ['sync'], { encoding: DEFAULT_BUFFER_ENCODING }); + expect(syncOutput.status).to.equal(0); + + const resultOne = spawnSync(`./${fileOneDestination}`, { + encoding: DEFAULT_BUFFER_ENCODING, + }); + expect(resultOne.stdout).to.contain('projectTest'); + expect(resultOne.stdout).to.contain('packageTestOne'); + expect(resultOne.stdout).to.contain(1); + + const resultTwo = spawnSync(`./${fileTwoDestination}`, { + encoding: DEFAULT_BUFFER_ENCODING, + }); + expect(resultTwo.stdout).to.contain('projectTest'); + expect(resultTwo.stdout).to.contain('packageTestTwo'); + expect(resultTwo.stdout).to.contain(2); + }); + }); +}); diff --git a/test/unit/project-config.test.ts b/test/unit/projectConfig.test.ts similarity index 61% rename from test/unit/project-config.test.ts rename to test/unit/projectConfig.test.ts index b31fb4df..f3cf9082 100644 --- a/test/unit/project-config.test.ts +++ b/test/unit/projectConfig.test.ts @@ -17,10 +17,10 @@ import 'mocha'; import { homedir } from 'node:os'; import { cwd } from 'node:process'; import sinon from 'sinon'; -import { ProjectConfig, ProjectConfigLock } from '../../src/modules/project-config'; +import { ProjectConfigIO } from '../../src/modules/projectConfig/projectConfigIO'; import { CliFileSystem, MockFileSystem, MockFileSystemObj } from '../../src/utils/fs-bridge'; -describe('project-config - module', () => { +describe('projectConfig - module', () => { const packageManifestPath = `${homedir()}/.velocitas/packages/pkg1/v1.0.0/manifest.json`; const validProjectConfigPath = `${cwd()}/.velocitasValid.json`; const validProjectConfigLockPath = `${cwd()}/.velocitasValid-lock.json`; @@ -28,10 +28,9 @@ describe('project-config - module', () => { const invalidProjectConfigPath = `${cwd()}/.velocitasInvalid.json`; before(() => { const mockFilesystem: MockFileSystemObj = { - [validProjectConfigPath]: - '{ "packages": [{"repo":"pkg1", "version": "v1.0.0"}], "components": [{"id": "comp1"}], "variables": {} }', - [validProjectConfigLockPath]: '{ "packages": [{"repo":"pkg1", "version": "v1.0.0"}] }', - [validProjectConfigNoCompsPath]: '{ "packages": [{"repo":"pkg1", "version": "v1.0.0"}], "variables": {} }', + [validProjectConfigPath]: '{ "packages": {"pkg1": "v1.0.0"}, "components": ["comp1"], "variables": {} }', + [validProjectConfigLockPath]: '{ "packages": {"pkg1": "v1.0.0"} }', + [validProjectConfigNoCompsPath]: '{ "packages": {"pkg1": "v1.0.0"}, "components": [], "variables": {} }', [invalidProjectConfigPath]: 'foo', [packageManifestPath]: '{ "components": [{"id": "comp1"}, {"id": "comp2"}]}', }; @@ -39,59 +38,59 @@ describe('project-config - module', () => { }); describe('.velocitas.json reading', () => { it('should return false when there is no .velocitas.json at the provided path.', () => { - expect(ProjectConfig.isAvailable('/.noVelocitas.json')).to.be.false; + expect(ProjectConfigIO.isConfigAvailable('/.noVelocitas.json')).to.be.false; }); it('should return true when there is a .velocitas.json at the provided path.', () => { - expect(ProjectConfig.isAvailable('/.velocitasValid.json')).to.be.true; + expect(ProjectConfigIO.isConfigAvailable('/.velocitasValid.json')).to.be.true; }); }); describe('.velocitas-lock.json reading', () => { it('should return false when there is no .velocitas-lock.json at the provided path.', () => { - expect(ProjectConfigLock.isAvailable('/.noVelocitas.json')).to.be.false; + expect(ProjectConfigIO.isLockAvailable('/.noVelocitas.json')).to.be.false; }); it('should return true when there is a .velocitas-lock.json at the provided path.', () => { - expect(ProjectConfigLock.isAvailable('/.velocitasValid-lock.json')).to.be.true; + expect(ProjectConfigIO.isLockAvailable('/.velocitasValid-lock.json')).to.be.true; }); }); describe('.velocitas.json parsing', () => { it('should throw an error when .velocitas.json is invalid.', () => { - expect(ProjectConfig.read.bind(ProjectConfig.read, ...['v0.0.0', './.velocitasInvalid.json'])).to.throw(); + expect(ProjectConfigIO.read.bind(ProjectConfigIO.read, ...['v0.0.0', './.velocitasInvalid.json'])).to.throw(); }); it('should read the ProjectConfig when .velocitas.json is valid.', () => { - expect(ProjectConfig.read.bind(ProjectConfig.read, ...['v0.0.0', './.velocitasValid.json'])).to.not.throw(); + expect(ProjectConfigIO.read.bind(ProjectConfigIO.read, ...['v0.0.0', './.velocitasValid.json'])).to.not.throw(); }); }); describe('.velocitas-lock.json parsing', () => { it('should throw an error when .velocitas-lock.json is invalid.', () => { - expect(() => ProjectConfigLock.read('./.velocitasInvalid.json')).to.throw(); + expect(() => ProjectConfigIO.readLock('./.velocitasInvalid.json')).to.throw(); }); it('should be null when no .velocitas-lock.json is found.', () => { - expect(ProjectConfigLock.read()).to.be.null; + expect(ProjectConfigIO.readLock()).to.be.null; }); it('should read the ProjectLockConfig when .velocitas-lock.json is valid.', () => { - expect(ProjectConfigLock.read('./.velocitasValid-lock.json')).to.not.be.null; + expect(ProjectConfigIO.readLock('./.velocitasValid-lock.json')).to.not.be.null; }); }); describe('.velocitas-lock.json writing', () => { it('should throw an error when writing to .velocitas-lock.json fails.', () => { - const projectConfig = ProjectConfig.read('v0.0.0', './.velocitasValid.json'); + const projectConfig = ProjectConfigIO.read('v0.0.0', './.velocitasValid.json'); const writeFileStub = sinon.stub(CliFileSystem, 'writeFileSync').throws('Mocked error'); - const writeFunction = () => ProjectConfigLock.write(projectConfig); + const writeFunction = () => ProjectConfigIO.writeLock(projectConfig); expect(writeFunction).to.throw('Error writing .velocitas-lock.json: Mocked error'); writeFileStub.restore(); }); }); describe('ProjectConfig components', () => { it('should only return referenced components', () => { - const projectConfig = ProjectConfig.read('v0.0.0', './.velocitasValid.json'); - expect(projectConfig.getComponents()).to.have.length(1); - expect(projectConfig.getComponents()[0].manifest.id).to.be.eq('comp1'); + const projectConfig = ProjectConfigIO.read('v0.0.0', './.velocitasValid.json'); + expect(projectConfig.getComponentContexts()).to.have.length(1); + expect(projectConfig.getComponentContexts()[0].manifest.id).to.be.eq('comp1'); }); it('should only return all components, if no components are referenced', () => { - const projectConfig = ProjectConfig.read('v0.0.0', './.velocitasValidNoComps.json'); - expect(projectConfig.getComponents()).to.have.length(2); - expect(projectConfig.getComponents()[0].manifest.id).to.be.eq('comp1'); - expect(projectConfig.getComponents()[1].manifest.id).to.be.eq('comp2'); + const projectConfig = ProjectConfigIO.read('v0.0.0', './.velocitasValidNoComps.json'); + expect(projectConfig.getComponentContexts()).to.have.length(2); + expect(projectConfig.getComponentContexts()[0].manifest.id).to.be.eq('comp1'); + expect(projectConfig.getComponentContexts()[1].manifest.id).to.be.eq('comp2'); }); }); }); diff --git a/test/unit/projectConfigIO.test.ts b/test/unit/projectConfigIO.test.ts new file mode 100644 index 00000000..840990e0 --- /dev/null +++ b/test/unit/projectConfigIO.test.ts @@ -0,0 +1,242 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from 'chai'; +import 'mocha'; +import { cwd } from 'node:process'; +import sinon from 'sinon'; +import { ComponentConfig } from '../../src/modules/component'; +import { PackageConfig } from '../../src/modules/package'; +import { ProjectConfigIO } from '../../src/modules/projectConfig/projectConfigIO'; +import { CliFileSystem, MockFileSystem, MockFileSystemObj } from '../../src/utils/fs-bridge'; + +const configFileMock = { + packages: { + 'test-package': 'v0.0.1', + }, + components: ['test-component'], + variables: { + projectVariable: 'projectTest', + ['packageVariable@test-package']: 'packageTest', + ['componentVariable@test-component']: 'componentTest', + }, + cliVersion: 'v0.0.1', +}; + +const configFileMockNoVariables = { + packages: { + 'test-package': 'v0.0.1', + }, + components: ['test-component'], + cliVersion: 'v0.0.1', +}; + +const configFileMockNoVariablesAndComponents = { + packages: { + 'test-package': 'v0.0.1', + }, + cliVersion: 'v0.0.1', +}; + +const configFileLockMock = { + packages: { + 'test-package': 'v0.0.2', + }, +}; + +const configFileLegacyMock = { + packages: [ + { + repo: 'test-package', + version: 'v0.0.1', + variables: { packageVariable: 'packageTest' }, + }, + ], + components: [{ id: 'test-component', variables: { componentVariable: 'componentTest' } }], + variables: { + projectVariable: 'projectTest', + }, + cliVersion: 'v0.0.1', +}; + +const configFileLegacyMockNoVariables = { + packages: [ + { + repo: 'test-package', + version: 'v0.0.1', + }, + ], + components: [{ id: 'test-component' }], + cliVersion: 'v0.0.1', +}; + +const configFilePath = `${cwd()}/.velocitas.json`; +const configFileLockPath = `${cwd()}/.velocitas-lock.json`; +const configFilePathNoVariables = `${cwd()}/.velocitasNoVariables.json`; +const configFilePathNoVariablesAndComponents = `${cwd()}/.velocitasNoVariablesAndComponents.json`; +const configFileLegacyPath = `${cwd()}/.velocitas-legacy.json`; +const configFileLegacyPathNoVariables = `${cwd()}/.velocitas-legacyNoVariables.json`; + +describe('projectConfigIO - module', () => { + before(() => { + const mockFilesystem: MockFileSystemObj = { + [configFilePath]: JSON.stringify(configFileMock), + [configFileLockPath]: JSON.stringify(configFileLockMock), + [configFilePathNoVariables]: JSON.stringify(configFileMockNoVariables), + [configFilePathNoVariablesAndComponents]: JSON.stringify(configFileMockNoVariablesAndComponents), + [configFileLegacyPath]: JSON.stringify(configFileLegacyMock), + [configFileLegacyPathNoVariables]: JSON.stringify(configFileLegacyMockNoVariables), + }; + CliFileSystem.setImpl(new MockFileSystem(mockFilesystem)); + }); + + describe('File Reading', () => { + it('should parse component configurations from the provided .velocitas.json file', () => { + const configFileObj = ProjectConfigIO.read('', configFilePath, true); + const projectConfigComponents = configFileObj.getComponents(); + expect(projectConfigComponents.every((cmp) => cmp instanceof ComponentConfig)).to.be.true; + expect(projectConfigComponents.length).to.equal(1); + expect(projectConfigComponents[0].id).to.equal('test-component'); + }); + + it('should assign variables to package configurations', () => { + const configFileObj = ProjectConfigIO.read('', configFilePath, true); + const projectConfigPackages = configFileObj.getPackages(); + const packageVariable = projectConfigPackages[0].variables.get('packageVariable'); + expect(packageVariable).to.equal('packageTest'); + }); + + it('should assign variables to component configurations', () => { + const configFileObj = ProjectConfigIO.read('', configFilePath, true); + const projectConfigComponents = configFileObj.getComponents(); + const componentVariable = projectConfigComponents[0].variables.get('componentVariable'); + expect(componentVariable).to.equal('componentTest'); + }); + + it('should assign empty maps to every variables property if no variables are provided', () => { + const configFileObj = ProjectConfigIO.read('', configFilePathNoVariables, true); + const projectConfigPackages = configFileObj.getPackages(); + const projectConfigComponents = configFileObj.getComponents(); + const projectConfigVariables = configFileObj.getVariableMappings(); + expect(projectConfigVariables).to.be.an.instanceOf(Map); + expect(projectConfigVariables.size).to.equal(0); + expect(projectConfigPackages[0].variables).to.be.an.instanceOf(Map); + expect(projectConfigPackages[0].variables.size).to.equal(0); + expect(projectConfigComponents[0].variables).to.be.an.instanceOf(Map); + expect(projectConfigComponents[0].variables.size).to.equal(0); + }); + + it('should assign an empty array to components configuration if no components are provided', () => { + const configFileObj = ProjectConfigIO.read('', configFilePathNoVariablesAndComponents, true); + const projectConfigComponents = configFileObj.getComponents(); + expect(projectConfigComponents).to.be.an('array'); + expect(projectConfigComponents.length).to.equal(0); + }); + + it('should handle project configuration lock if available', () => { + const configFileObj = ProjectConfigIO.read('', configFilePath, false); + const projectConfigPackages = configFileObj.getPackages(); + expect(projectConfigPackages.every((pkg) => pkg instanceof PackageConfig)).to.be.true; + expect(projectConfigPackages.length).to.equal(1); + expect(projectConfigPackages[0].repo).to.equal('test-package'); + expect(projectConfigPackages[0].version).to.equal('v0.0.2'); + }); + + it('should correctly store the CLI version from .velocitas.json file', () => { + const configFileObj = ProjectConfigIO.read('', configFilePath, true); + expect(configFileObj.cliVersion).to.equal('v0.0.1'); + }); + }); + + describe('Legacy File Reading', () => { + it('should parse component configurations from the provided .velocitas.json file', () => { + const configFileObj = ProjectConfigIO.read('', configFileLegacyPath, true); + const projectConfigComponents = configFileObj.getComponents(); + expect(projectConfigComponents.every((cmp) => cmp instanceof ComponentConfig)).to.be.true; + expect(projectConfigComponents.length).to.equal(1); + expect(projectConfigComponents[0].id).to.equal('test-component'); + }); + + it('should assign variables to package configurations', () => { + const configFileObj = ProjectConfigIO.read('', configFileLegacyPath, true); + const projectConfigPackages = configFileObj.getPackages(); + const packageVariable = projectConfigPackages[0].variables.get('packageVariable'); + expect(packageVariable).to.equal('packageTest'); + }); + + it('should assign variables to component configurations', () => { + const configFileObj = ProjectConfigIO.read('', configFileLegacyPath, true); + const projectConfigComponents = configFileObj.getComponents(); + const componentVariable = projectConfigComponents[0].variables.get('componentVariable'); + expect(componentVariable).to.equal('componentTest'); + }); + + it('should assign empty maps to every variables property if no variables are provided', () => { + const configFileObj = ProjectConfigIO.read('', configFileLegacyPathNoVariables, true); + const projectConfigPackages = configFileObj.getPackages(); + const projectConfigComponents = configFileObj.getComponents(); + const projectConfigVariables = configFileObj.getVariableMappings(); + expect(projectConfigVariables).to.be.an.instanceOf(Map); + expect(projectConfigVariables.size).to.equal(0); + expect(projectConfigPackages[0].variables).to.be.an.instanceOf(Map); + expect(projectConfigPackages[0].variables.size).to.equal(0); + expect(projectConfigComponents[0].variables).to.be.an.instanceOf(Map); + expect(projectConfigComponents[0].variables.size).to.equal(0); + }); + + it('should assign an empty array to components configuration if no components are provided', () => { + const configFileObj = ProjectConfigIO.read('', configFilePathNoVariablesAndComponents, true); + const projectConfigComponents = configFileObj.getComponents(); + expect(projectConfigComponents).to.be.an('array'); + expect(projectConfigComponents.length).to.equal(0); + }); + + it('should handle project configuration lock if available', () => { + const configFileObj = ProjectConfigIO.read('', configFileLegacyPath, false); + const projectConfigPackages = configFileObj.getPackages(); + expect(projectConfigPackages.every((pkg) => pkg instanceof PackageConfig)).to.be.true; + expect(projectConfigPackages.length).to.equal(1); + expect(projectConfigPackages[0].repo).to.equal('test-package'); + expect(projectConfigPackages[0].version).to.equal('v0.0.2'); + }); + + it('should correctly store the CLI version from .velocitas.json file', () => { + const configFileObj = ProjectConfigIO.read('', configFileLegacyPath, true); + expect(configFileObj.cliVersion).to.equal('v0.0.1'); + }); + }); + + describe('Error handling', () => { + it('should handle errors when parsing .velocitas.json file', () => { + const readFileStub = sinon.stub(CliFileSystem, 'readFileSync').throws(); + const projectConfigFileReader = () => ProjectConfigIO.read('', configFilePath, true); + expect(projectConfigFileReader).to.throw(`Unable to read ${configFilePath}: unknown format!`); + readFileStub.restore(); + }); + + it('should handle errors when lock file is not found', () => { + const readLockFileStub = sinon.stub(ProjectConfigIO, 'readLock').returns(null); + const projectConfigLockFileReader = () => ProjectConfigIO.readLock(configFileLockPath); + expect(projectConfigLockFileReader).not.to.throw(); + readLockFileStub.restore(); + }); + + it('should handle errors when reading lock file', () => { + const readLockFileStub = sinon.stub(ProjectConfigIO, 'readLock').throws(new Error('Lock file error')); + const projectConfigLockFileReader = () => ProjectConfigIO.readLock(configFileLockPath); + expect(projectConfigLockFileReader).to.throw('Lock file error'); + readLockFileStub.restore(); + }); + }); +}); diff --git a/test/unit/semver.test.ts b/test/unit/semver.test.ts index 5f89d4e2..5e8c3dcb 100644 --- a/test/unit/semver.test.ts +++ b/test/unit/semver.test.ts @@ -36,7 +36,7 @@ describe('resolveVersionIdentifier', () => { }; it('should resolve to a branch if versionIdentifier starts with "@"', () => { - expect(resolveVersionIdentifier(versions, '@myBranch')).to.equal('myBranch'); + expect(resolveVersionIdentifier(versions, '@myBranch')).to.equal('@myBranch'); }); it('should resolve to the latest version if versionIdentifier is "latest"', () => { diff --git a/test/unit/variables.test.ts b/test/unit/variables.test.ts index 23b99598..812ba2c7 100644 --- a/test/unit/variables.test.ts +++ b/test/unit/variables.test.ts @@ -16,32 +16,51 @@ import { expect } from 'chai'; import 'mocha'; import { ComponentConfig, ComponentContext, ComponentManifest } from '../../src/modules/component'; import { PackageConfig } from '../../src/modules/package'; -import { ProjectConfig } from '../../src/modules/project-config'; +import { ProjectConfig } from '../../src/modules/projectConfig/projectConfig'; import { ScopeIdentifier, VariableCollection } from '../../src/modules/variables'; let projectConfig: ProjectConfig; -let pkg1Config: PackageConfig; -let pkg2Config: PackageConfig; let pkg1Comp1Cfg: ComponentConfig; let pkg1Comp1Manifest: ComponentManifest; let pkg2Comp1Manifest: ComponentManifest; -let pkg2Comp2Cfg: ComponentConfig; let pkg2Comp2Manifest: ComponentManifest; -let variablesObject: { [key: string]: any }; -let variablesMap: Map; + let componentContext: ComponentContext; let componentContext2: ComponentContext; let componentContext3: ComponentContext; +let variablesMapProjCfg: Map; + +// content of velocitas.json +const variablesObjectProjCfg: { [key: string]: any } = { + [`testString@test-package`]: 'test', + [`testNumber@test-package`]: 1, + [`testString@test-component`]: 'test', + [`testNumber@test-component`]: 1, +}; + +const variablesObject: { [key: string]: any } = { + testString: 'test', + testNumber: 1, +}; describe('variables - module', () => { beforeEach(() => { - variablesObject = { testString: 'test', testNumber: 1 }; - variablesMap = new Map(Object.entries(variablesObject)); - pkg1Config = new PackageConfig({ repo: 'test-package', version: 'v1.1.1', variables: variablesMap }); - pkg2Config = new PackageConfig({ repo: 'test-package2', version: 'v0.0.1' }); - projectConfig = new ProjectConfig('v0.0.0', { packages: [pkg1Config], variables: variablesMap }); + variablesMapProjCfg = new Map(Object.entries(variablesObjectProjCfg)); + const variablesMapPkgCfg: Map = new Map(Object.entries(variablesObject)); + const variablesMapCmpCfg: Map = new Map(Object.entries(variablesObject)); + + const pkg1Config: PackageConfig = new PackageConfig({ repo: 'test-package', version: 'v1.1.1', variables: variablesMapPkgCfg }); + const pkg2Config: PackageConfig = new PackageConfig({ repo: 'test-package2', version: 'v0.0.1' }); + pkg1Comp1Cfg = new ComponentConfig('test-component'); + pkg1Comp1Cfg.variables = variablesMapCmpCfg; + + projectConfig = new ProjectConfig('v0.0.0', { + packages: [pkg1Config, pkg2Config], + components: [pkg1Comp1Cfg], + variables: variablesMapProjCfg, + cliVersion: '', + }); - pkg1Comp1Cfg = { id: 'test-component', variables: variablesMap }; pkg1Comp1Manifest = { id: 'test-component', variables: [ @@ -94,25 +113,40 @@ describe('variables - module', () => { variables: [], }; - componentContext = new ComponentContext(pkg1Config, pkg1Comp1Manifest, pkg1Comp1Cfg, true); - componentContext2 = new ComponentContext(pkg2Config, pkg2Comp1Manifest, new ComponentConfig(pkg2Comp1Manifest.id), true); - componentContext3 = new ComponentContext(pkg2Config, pkg2Comp2Manifest, new ComponentConfig(pkg2Comp2Manifest.id), true); + componentContext = new ComponentContext( + projectConfig.getPackages()[0], + pkg1Comp1Manifest, + projectConfig.getComponentConfig(pkg1Comp1Cfg.id), + true, + ); + componentContext2 = new ComponentContext( + projectConfig.getPackages()[1], + pkg2Comp1Manifest, + new ComponentConfig(pkg2Comp1Manifest.id), + true, + ); + componentContext3 = new ComponentContext( + projectConfig.getPackages()[1], + pkg2Comp2Manifest, + new ComponentConfig(pkg2Comp2Manifest.id), + true, + ); }); describe('VariableCollection', () => { it('should build a VariableCollection with given mocks', () => { - const variableCollection = VariableCollection.build([componentContext], variablesMap, componentContext); + const variableCollection = VariableCollection.build([componentContext], variablesMapProjCfg, componentContext); expect(variableCollection).to.exist; expect(variableCollection).to.be.an.instanceof(VariableCollection); }); it('should skip verifyGivenVariables when component does not expect variables', () => { pkg1Comp1Manifest.variables = []; - const variableCollection = VariableCollection.build([componentContext], variablesMap, componentContext); + const variableCollection = VariableCollection.build([componentContext], variablesMapProjCfg, componentContext); expect(variableCollection).to.exist; expect(variableCollection).to.be.an.instanceof(VariableCollection); }); it('should throw an error when component expects required variables which are not configured', () => { - pkg1Config.variables = new Map(); - pkg1Comp1Cfg.variables = new Map(); + projectConfig.getComponentConfig(pkg1Comp1Cfg.id).variables = new Map(); + projectConfig.getPackages()[0].variables = new Map(); let expectedErrorMessage: string = ''; expectedErrorMessage += `'${pkg1Comp1Cfg.id}' has issues with its configured variables:\n`; expectedErrorMessage += `Is missing required variables:\n`; @@ -124,20 +158,32 @@ describe('variables - module', () => { expectedErrorMessage += `\tType: ${pkg1Comp1Manifest.variables[1].type}\n`; expectedErrorMessage += `\t${pkg1Comp1Manifest.variables[1].description}`; } + componentContext = new ComponentContext( + projectConfig.getPackages()[0], + pkg1Comp1Manifest, + projectConfig.getComponentConfig(pkg1Comp1Cfg.id), + true, + ); expect(() => VariableCollection.build([componentContext], new Map(), componentContext)).to.throw(expectedErrorMessage); }); it('should throw an error when exposed component variable has wrong type', () => { - projectConfig.getPackages()[0].variables?.set('testNumber', 'wrongType'); + projectConfig.getComponentConfig(pkg1Comp1Cfg.id).variables.set('testNumber', 'wrongType'); let expectedErrorMessage: string = ''; expectedErrorMessage += `'${pkg1Comp1Cfg.id}' has issues with its configured variables:\n`; expectedErrorMessage += `Has wrongly typed variables:\n`; if (pkg1Comp1Manifest.variables) { expectedErrorMessage += `* '${pkg1Comp1Manifest.variables[1].name}' has wrong type! Expected ${pkg1Comp1Manifest.variables[1].type} but got string`; } - expect(() => VariableCollection.build([componentContext], variablesMap, componentContext)).to.throw(expectedErrorMessage); + expect(() => VariableCollection.build([componentContext], variablesMapProjCfg, componentContext)).to.throw( + expectedErrorMessage, + ); }); it('should allow project scope variables to be passed to other components', () => { - const variableCollection = VariableCollection.build([componentContext, componentContext2], variablesMap, componentContext); + const variableCollection = VariableCollection.build( + [componentContext, componentContext2], + variablesMapProjCfg, + componentContext, + ); expect(variableCollection).to.exist; expect(variableCollection).to.be.an.instanceof(VariableCollection); expect(variableCollection.substitute('${{ exportedStringConst }}')).to.equal('PROJECT_EXPORTED'); @@ -145,12 +191,12 @@ describe('variables - module', () => { it('should allow package scope variables to be passed to other components within the same package', () => { const variableCollection1 = VariableCollection.build( [componentContext, componentContext2, componentContext3], - variablesMap, + variablesMapProjCfg, componentContext3, ); const variableCollection2 = VariableCollection.build( [componentContext, componentContext2, componentContext3], - variablesMap, + variablesMapProjCfg, componentContext, ); expect(variableCollection1).to.exist; @@ -177,7 +223,7 @@ describe('variables - module', () => { // expect(() => VariableCollection.build(projectConfig, packageConfig, componentConfig, component)).to.throw(expectedErrorMessage); }); it('should provide builtin variables', () => { - const vars = VariableCollection.build([componentContext], variablesMap, componentContext); + const vars = VariableCollection.build([componentContext], variablesMapProjCfg, componentContext); expect(vars.substitute('${{ builtin.package.version }}')).to.equal('v1.1.1'); expect(vars.substitute('${{ builtin.package.github.org }}')).to.equal('eclipse-velocitas'); @@ -186,7 +232,7 @@ describe('variables - module', () => { expect(vars.substitute('${{ builtin.component.id }}')).to.equal('test-component'); }); it('should transform variable names into allowed environment variable names', () => { - const vars = VariableCollection.build([componentContext], variablesMap, componentContext); + const vars = VariableCollection.build([componentContext], variablesMapProjCfg, componentContext); const envVars = vars.asEnvVars(); expect(envVars['builtin_package_version']).to.equal('v1.1.1'); @@ -203,11 +249,17 @@ describe('variables - module', () => { description: 'This is a test duplicate', }; pkg1Comp1Manifest.variables?.push(alreadyExistingVariableDefName); - const componentContextWithMultipleVariableDef = new ComponentContext(pkg1Config, pkg1Comp1Manifest, pkg1Comp1Cfg, true); + + const componentContextWithMultipleVariableDef = new ComponentContext( + projectConfig.getPackages()[0], + pkg1Comp1Manifest, + projectConfig.getComponentConfig(pkg1Comp1Cfg.id), + true, + ); return VariableCollection.build( [componentContextWithMultipleVariableDef], - variablesMap, + variablesMapProjCfg, componentContextWithMultipleVariableDef, ); }; diff --git a/test/utils/mockConfig.ts b/test/utils/mockConfig.ts index 479a52ce..7fa7a9f8 100644 --- a/test/utils/mockConfig.ts +++ b/test/utils/mockConfig.ts @@ -15,60 +15,42 @@ import { CoreComponent, ExtensionComponent, PackageAttributes } from '../../src/modules/package-index'; import { ScopeIdentifier } from '../../src/modules/variables'; +export const runtimePackageInfoMock = { + repo: 'test-runtime', + versionIdentifier: 'v1.1.*', + resolvedVersion: 'v1.1.1', +}; +export const setupPackageInfoMock = { + repo: 'test-setup', + versionIdentifier: 'v1.1.*', + resolvedVersion: 'v1.1.1', +}; +export const corePackageInfoMock = { + repo: 'test-package-main', + versionIdentifier: 'v1.1.*', + resolvedVersion: 'v1.1.1', +}; + export const velocitasConfigMock = { - packages: [ - { - repo: 'test-runtime', - version: 'v1.1.*', - variables: { test: 'test' }, - }, - { - repo: 'test-setup', - version: 'v1.1.*', - }, - { - repo: 'test-package-main', - version: 'v1.1.*', - }, - ], - components: [ - { - id: 'test-runtime-local', - }, - { - id: 'test-runtime-deploy-local', - }, - { - id: 'github-workflows', - }, - { - id: 'core-test', - }, - { - id: 'test-extension-mandatory', - }, - ], + packages: { + [runtimePackageInfoMock.repo]: runtimePackageInfoMock.versionIdentifier, + [setupPackageInfoMock.repo]: setupPackageInfoMock.versionIdentifier, + [corePackageInfoMock.repo]: corePackageInfoMock.versionIdentifier, + }, + components: ['test-runtime-local', 'test-runtime-deploy-local', 'github-workflows', 'core-test', 'test-extension-mandatory'], variables: { appManifestPath: './app/AppManifest.json', githubRepoId: 'myRepo', + [`test@${runtimePackageInfoMock.repo}`]: 'test', }, }; export const velocitasConfigLockMock = { - packages: [ - { - repo: 'test-runtime', - version: 'v1.1.1', - }, - { - repo: 'test-setup', - version: 'v1.1.1', - }, - { - repo: 'test-package-main', - version: 'v1.1.1', - }, - ], + packages: { + [runtimePackageInfoMock.repo]: runtimePackageInfoMock.resolvedVersion, + [setupPackageInfoMock.repo]: setupPackageInfoMock.resolvedVersion, + [corePackageInfoMock.repo]: corePackageInfoMock.resolvedVersion, + }, }; export const packageIndexMock: PackageAttributes[] = [ diff --git a/test/utils/mockfs.ts b/test/utils/mockfs.ts index d4f92ede..4945208b 100644 --- a/test/utils/mockfs.ts +++ b/test/utils/mockfs.ts @@ -18,32 +18,23 @@ import { ProjectCache } from '../../src/modules/project-cache'; import { CliFileSystem, MockFileSystem, MockFileSystemObj } from '../../src/utils/fs-bridge'; import { appManifestMock, + corePackageInfoMock, corePackageManifestMock, mockCacheContent, packageIndexMock, + runtimePackageInfoMock, runtimePackageManifestMock, + setupPackageInfoMock, setupPackageManifestMock, velocitasConfigLockMock, velocitasConfigMock, } from './mockConfig'; export const userHomeDir = os.homedir(); -export const installedRuntimePackage = { - repo: velocitasConfigLockMock.packages[0].repo, - version: velocitasConfigLockMock.packages[0].version, -}; -export const installedSetupPackage = { - repo: velocitasConfigLockMock.packages[1].repo, - version: velocitasConfigLockMock.packages[1].version, -}; -export const installedCorePackage = { - repo: velocitasConfigLockMock.packages[2].repo, - version: velocitasConfigLockMock.packages[2].version, -}; -const runtimePackagePath = `${userHomeDir}/.velocitas/packages/${installedRuntimePackage.repo}/${installedRuntimePackage.version}`; -const setupPackagePath = `${userHomeDir}/.velocitas/packages/${installedSetupPackage.repo}/${installedSetupPackage.version}`; -const corePackagePath = `${userHomeDir}/.velocitas/packages/${installedCorePackage.repo}/${installedCorePackage.version}`; +const runtimePackagePath = `${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}`; +const setupPackagePath = `${userHomeDir}/.velocitas/packages/${setupPackageInfoMock.repo}/${setupPackageInfoMock.resolvedVersion}`; +const corePackagePath = `${userHomeDir}/.velocitas/packages/${corePackageInfoMock.repo}/${setupPackageInfoMock.resolvedVersion}`; const velocitasConfig = `${cwd()}/.velocitas.json`; const velocitasConfigLock = `${cwd()}/.velocitas-lock.json`; diff --git a/testbench/test-exec/.velocitas.json b/testbench/test-exec/.velocitas.json index 19a31cc2..f06e42a9 100644 --- a/testbench/test-exec/.velocitas.json +++ b/testbench/test-exec/.velocitas.json @@ -1,14 +1,8 @@ { - "packages": [ - { - "name": "devenv-runtimes", - "version": "v3.0.0" - }, - { - "name": "test-package", - "version": "test-version" - } - ], + "packages": { + "devenv-runtimes": "v3.0.0", + "test-package": "test-version" + }, "variables": { "language": "python", "repoType": "app", diff --git a/testbench/test-init/.velocitasInvalidComponent.json b/testbench/test-init/.velocitasInvalidComponent.json index 23109715..dc937826 100644 --- a/testbench/test-init/.velocitasInvalidComponent.json +++ b/testbench/test-init/.velocitasInvalidComponent.json @@ -1,14 +1,9 @@ { - "packages": [ - { - "repo": "https://github.com/eclipse-velocitas/devenv-devcontainer-setup.git", - "version": "v1.5.0" - } - ], + "packages": { + "https://github.com/eclipse-velocitas/devenv-devcontainer-setup.git": "v1.5.0" + }, "components": [ - { - "id": "invalid-component" - } + "invalid-component" ], "variables": { "appManifestPath": "./app/AppManifest.json", diff --git a/testbench/test-init/.velocitasOld.json b/testbench/test-init/.velocitasLegacy.json similarity index 73% rename from testbench/test-init/.velocitasOld.json rename to testbench/test-init/.velocitasLegacy.json index 25c07dee..59ff6891 100644 --- a/testbench/test-init/.velocitasOld.json +++ b/testbench/test-init/.velocitasLegacy.json @@ -1,19 +1,19 @@ { "packages": [ { - "name": "devenv-runtimes", + "repo": "devenv-runtimes", "version": "v2.2.6" }, { - "name": "devenv-github-workflows", + "repo": "devenv-github-workflows", "version": "v4.1.4" }, { - "name": "devenv-github-templates", + "repo": "devenv-github-templates", "version": "v1.0.3" }, { - "name": "devenv-devcontainer-setup", + "repo": "devenv-devcontainer-setup", "version": "v1.4.7" } ], diff --git a/testbench/test-init/.velocitasNew.json b/testbench/test-init/.velocitasNew.json index 5464f482..c9fb49ba 100644 --- a/testbench/test-init/.velocitasNew.json +++ b/testbench/test-init/.velocitasNew.json @@ -1,57 +1,22 @@ { - "packages": [ - { - "repo": "https://github.com/eclipse-velocitas/pkg-velocitas-main.git", - "version": "v0.0.2" - }, - { - "repo": "https://github.com/eclipse-velocitas/devenv-devcontainer-setup.git", - "version": "v2.0.0" - }, - { - "repo": "https://github.com/eclipse-velocitas/devenv-runtimes.git", - "version": "v3.0.0" - }, - { - "repo": "https://github.com/eclipse-velocitas/devenv-github-templates.git", - "version": "v1.0.3" - }, - { - "repo": "https://github.com/eclipse-velocitas/devenv-github-workflows.git", - "version": "v5.0.0" - } - ], + "packages": { + "https://github.com/eclipse-velocitas/pkg-velocitas-main.git": "v0.0.2", + "https://github.com/eclipse-velocitas/devenv-devcontainer-setup.git": "v2.0.0", + "https://github.com/eclipse-velocitas/devenv-runtimes.git": "v3.0.0", + "https://github.com/eclipse-velocitas/devenv-github-templates.git": "v1.0.3", + "https://github.com/eclipse-velocitas/devenv-github-workflows.git": "v5.0.0" + }, "components": [ - { - "id": "vapp-core-python" - }, - { - "id": "vehicle-signal-interface" - }, - { - "id": "devcontainer-setup" - }, - { - "id": "sdk-installer" - }, - { - "id": "runtime-local" - }, - { - "id": "runtime-kanto" - }, - { - "id": "deployment-kanto" - }, - { - "id": "pantaris-integration" - }, - { - "id": "github-templates" - }, - { - "id": "github-workflows" - } + "vapp-core-python", + "vehicle-signal-interface", + "devcontainer-setup", + "sdk-installer", + "runtime-local", + "runtime-kanto", + "deployment-kanto", + "pantaris-integration", + "github-templates", + "github-workflows" ], "variables": { "appManifestPath": "./app/AppManifest.json", diff --git a/testbench/test-sync/.velocitas.json b/testbench/test-sync/.velocitas.json new file mode 100644 index 00000000..fe49b086 --- /dev/null +++ b/testbench/test-sync/.velocitas.json @@ -0,0 +1,17 @@ +{ + "packages": { + "test-packageOne": "test-version", + "test-packageTwo": "test-version" + }, + "components": [ + "test-componentOne", + "test-componentTwo" + ], + "variables": { + "projectVariable": "projectTest", + "packageVariable@test-packageOne": "packageTestOne", + "packageVariable@test-packageTwo": "packageTestTwo", + "componentVariable@test-componentOne": 1, + "componentVariable@test-componentTwo": 2 + } +} diff --git a/testbench/test-sync/packages/test-packageOne/test-version/manifest.json b/testbench/test-sync/packages/test-packageOne/test-version/manifest.json new file mode 100644 index 00000000..1b0cf230 --- /dev/null +++ b/testbench/test-sync/packages/test-packageOne/test-version/manifest.json @@ -0,0 +1,14 @@ +{ + "components": [ + { + "id": "test-componentOne", + "type": "setup", + "files": [ + { + "src": "testFile.sh", + "dst": "testFile1.sh" + } + ] + } + ] +} diff --git a/testbench/test-sync/packages/test-packageOne/test-version/testFile.sh b/testbench/test-sync/packages/test-packageOne/test-version/testFile.sh new file mode 100755 index 00000000..df32b78e --- /dev/null +++ b/testbench/test-sync/packages/test-packageOne/test-version/testFile.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +echo ${{ projectVariable }} +echo ${{ packageVariable }} +echo ${{ componentVariable }} diff --git a/testbench/test-sync/packages/test-packageTwo/test-version/manifest.json b/testbench/test-sync/packages/test-packageTwo/test-version/manifest.json new file mode 100644 index 00000000..a7f726cd --- /dev/null +++ b/testbench/test-sync/packages/test-packageTwo/test-version/manifest.json @@ -0,0 +1,14 @@ +{ + "components": [ + { + "id": "test-componentTwo", + "type": "setup", + "files": [ + { + "src": "testFile.sh", + "dst": "testFile2.sh" + } + ] + } + ] +} diff --git a/testbench/test-sync/packages/test-packageTwo/test-version/testFile.sh b/testbench/test-sync/packages/test-packageTwo/test-version/testFile.sh new file mode 100755 index 00000000..df32b78e --- /dev/null +++ b/testbench/test-sync/packages/test-packageTwo/test-version/testFile.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +echo ${{ projectVariable }} +echo ${{ packageVariable }} +echo ${{ componentVariable }}