From ed62502e9b4b55fbf933623e65609b0f64c0295d Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Tue, 25 Jul 2023 10:56:49 -0600 Subject: [PATCH 01/15] Add support for scanning parent directories for configuration file --- .../dev-environment/dev-environment-cli.ts | 4 +- .../dev-environment-configuration-file.ts | 46 +++++++++++++++---- src/lib/dev-environment/types.ts | 2 + 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/lib/dev-environment/dev-environment-cli.ts b/src/lib/dev-environment/dev-environment-cli.ts index af8c0e48f..46bccbc43 100644 --- a/src/lib/dev-environment/dev-environment-cli.ts +++ b/src/lib/dev-environment/dev-environment-cli.ts @@ -217,7 +217,7 @@ export async function getEnvironmentName( options: EnvironmentNameOptions ): Pro const slug = configurationFileOptions.slug; console.log( `Using environment ${ chalk.blue.bold( slug ) } from ${ chalk.gray( - CONFIGURATION_FILE_NAME + configurationFileOptions[ 'configuration-path' ] ) }\n` ); @@ -673,7 +673,7 @@ export function resolvePhpVersion( version: string ): string { let result: string; if ( DEV_ENVIRONMENT_PHP_VERSIONS[ version ] === undefined ) { - const images = Object.values( DEV_ENVIRONMENT_PHP_VERSIONS ) as string[]; + const images = Object.values( DEV_ENVIRONMENT_PHP_VERSIONS ); const image = images.find( value => value === version ); if ( image ) { result = image; diff --git a/src/lib/dev-environment/dev-environment-configuration-file.ts b/src/lib/dev-environment/dev-environment-configuration-file.ts index 6e4b839ac..82ad2a872 100644 --- a/src/lib/dev-environment/dev-environment-configuration-file.ts +++ b/src/lib/dev-environment/dev-environment-configuration-file.ts @@ -18,20 +18,15 @@ const debug = debugLib( '@automattic/vip:bin:dev-environment' ); export const CONFIGURATION_FILE_NAME = '.vip-dev-env.yml'; export async function getConfigurationFileOptions(): Promise< ConfigurationFileOptions > { - const configurationFilePath = path.join( process.cwd(), CONFIGURATION_FILE_NAME ); - let configurationFileContents = ''; + const configurationFilePath = await findConfigurationFilePath(); - const fileExists = await access( configurationFilePath, constants.R_OK ) - .then( () => true ) - .catch( () => false ); - - if ( fileExists ) { - debug( 'Reading configuration file from:', configurationFilePath ); - configurationFileContents = await readFile( configurationFilePath, 'utf8' ); - } else { + if ( configurationFilePath === false ) { return {}; } + debug( 'Reading configuration file from:', configurationFilePath ); + const configurationFileContents = await readFile( configurationFilePath, 'utf8' ); + let configurationFromFile: Record< string, unknown > = {}; try { @@ -49,6 +44,8 @@ export async function getConfigurationFileOptions(): Promise< ConfigurationFileO try { const configuration = sanitizeConfiguration( configurationFromFile ); + configuration[ 'configuration-path' ] = configurationFilePath; + debug( 'Sanitized configuration from file:', configuration ); return configuration; } catch ( err ) { @@ -184,6 +181,35 @@ export function printConfigurationFile( configurationOptions: ConfigurationFileO console.log( settingLines.join( '\n' ) + '\n' ); } +async function findConfigurationFilePath(): Promise< string | false > { + let currentPath = process.cwd(); + const rootPath = path.parse( currentPath ).root; + + let depth = 0; + const maxDepth = 32; + const pathPromises = []; + + while ( currentPath !== rootPath && depth < maxDepth ) { + const configurationFilePath = path.join( currentPath, CONFIGURATION_FILE_NAME ); + + pathPromises.push( + access( configurationFilePath, constants.R_OK ).then( () => configurationFilePath ) + ); + + // Move up one directory + currentPath = path.dirname( currentPath ); + + // Use depth as a sanity check to avoid an infitite loop + depth++; + } + + return Promise.any( pathPromises ) + .then( configurationFilePath => { + return configurationFilePath; + } ) + .catch( () => false ); +} + const CONFIGURATION_FILE_VERSIONS = [ '0.preview-unstable' ]; function getAllConfigurationFileVersions(): string[] { diff --git a/src/lib/dev-environment/types.ts b/src/lib/dev-environment/types.ts index bc6633289..80d6bcfe3 100644 --- a/src/lib/dev-environment/types.ts +++ b/src/lib/dev-environment/types.ts @@ -74,6 +74,8 @@ export interface ConfigurationFileOptions { 'media-redirect-domain'?: string; photon?: boolean; + 'configuration-path'?: string; + [ index: string ]: unknown; } From 13a7a53f2cd320b469e789de61e2c4b20b96ebdc Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Tue, 25 Jul 2023 11:37:00 -0600 Subject: [PATCH 02/15] Rename configuration file, add parent directory configuration file test --- ...spec.js => 013-configuration-file.spec.js} | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) rename __tests__/devenv-e2e/{011-configuration-file.spec.js => 013-configuration-file.spec.js} (85%) diff --git a/__tests__/devenv-e2e/011-configuration-file.spec.js b/__tests__/devenv-e2e/013-configuration-file.spec.js similarity index 85% rename from __tests__/devenv-e2e/011-configuration-file.spec.js rename to __tests__/devenv-e2e/013-configuration-file.spec.js index 5eb6f466a..232e10f06 100644 --- a/__tests__/devenv-e2e/011-configuration-file.spec.js +++ b/__tests__/devenv-e2e/013-configuration-file.spec.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp, mkdir, rm } from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { describe, expect, it, jest } from '@jest/globals'; @@ -121,6 +121,39 @@ describe( 'vip dev-env configuration file', () => { return expect( checkEnvExists( expectedSlug ) ).resolves.toBe( true ); } ); + it( 'should create a new environment under parent directories', async () => { + const workingDirectoryPath = await mkdtemp( path.join( os.tmpdir(), 'vip-dev-env-working-' ) ); + const workingDirectoryChildPath1 = path.join( workingDirectoryPath, 'child-folder1' ); + const workingDirectoryChildPath2 = path.join( workingDirectoryChildPath1, 'child-folder2' ); + await mkdir( workingDirectoryChildPath2, { recursive: true } ); + + const expectedSlug = getProjectSlug(); + expect( await checkEnvExists( expectedSlug ) ).toBe( false ); + + // Write configuration file in top working directory + await writeConfigurationFile( workingDirectoryPath, { + 'configuration-version': '0.preview-unstable', + slug: expectedSlug, + } ); + + // Spawn in a directory two folders below the configuration file + const spawnOptions = { + env, + cwd: workingDirectoryChildPath2, + }; + + const result = await cliTest.spawn( + [ process.argv[ 0 ], vipDevEnvCreate ], + spawnOptions, + true + ); + expect( result.rc ).toBe( 0 ); + expect( result.stdout ).toContain( `Using environment ${ expectedSlug }` ); + expect( result.stderr ).toBe( '' ); + + return expect( checkEnvExists( expectedSlug ) ).resolves.toBe( true ); + } ); + it( 'should create a new environment with full configuration from file', async () => { const expectedSlug = getProjectSlug(); const expectedTitle = 'Test'; From 1b5a6d8667787ed327a899adb92837452bec9bd0 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Tue, 25 Jul 2023 11:51:08 -0600 Subject: [PATCH 03/15] Refactor grandparent directory test --- __tests__/devenv-e2e/013-configuration-file.spec.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/__tests__/devenv-e2e/013-configuration-file.spec.js b/__tests__/devenv-e2e/013-configuration-file.spec.js index 232e10f06..8ed4eb65d 100644 --- a/__tests__/devenv-e2e/013-configuration-file.spec.js +++ b/__tests__/devenv-e2e/013-configuration-file.spec.js @@ -121,11 +121,10 @@ describe( 'vip dev-env configuration file', () => { return expect( checkEnvExists( expectedSlug ) ).resolves.toBe( true ); } ); - it( 'should create a new environment under parent directories', async () => { + it( 'should create a new environment using grandparent directory configuration', async () => { const workingDirectoryPath = await mkdtemp( path.join( os.tmpdir(), 'vip-dev-env-working-' ) ); - const workingDirectoryChildPath1 = path.join( workingDirectoryPath, 'child-folder1' ); - const workingDirectoryChildPath2 = path.join( workingDirectoryChildPath1, 'child-folder2' ); - await mkdir( workingDirectoryChildPath2, { recursive: true } ); + const workingDirectoryGrandchild = path.join( workingDirectoryPath, 'child', 'grand-child' ); + await mkdir( workingDirectoryGrandchild, { recursive: true } ); const expectedSlug = getProjectSlug(); expect( await checkEnvExists( expectedSlug ) ).toBe( false ); @@ -139,7 +138,7 @@ describe( 'vip dev-env configuration file', () => { // Spawn in a directory two folders below the configuration file const spawnOptions = { env, - cwd: workingDirectoryChildPath2, + cwd: workingDirectoryGrandchild, }; const result = await cliTest.spawn( From d700f4fd93afeea5760d1ba48ef2b79dacb5048f Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 31 Jul 2023 11:50:13 -0600 Subject: [PATCH 04/15] Add failing test for relative configuration file paths --- .../devenv-e2e/013-configuration-file.spec.js | 50 ++++++++++++++++++- __tests__/devenv-e2e/helpers/utils.js | 24 ++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/__tests__/devenv-e2e/013-configuration-file.spec.js b/__tests__/devenv-e2e/013-configuration-file.spec.js index 8ed4eb65d..21dd382e6 100644 --- a/__tests__/devenv-e2e/013-configuration-file.spec.js +++ b/__tests__/devenv-e2e/013-configuration-file.spec.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { mkdtemp, mkdir, rm } from 'node:fs/promises'; +import { mkdtemp, mkdir, rm, realpath } from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { describe, expect, it, jest } from '@jest/globals'; @@ -17,6 +17,7 @@ import { getProjectSlug, prepareEnvironment, writeConfigurationFile, + makeRequiredAppCodeDirectories, } from './helpers/utils'; import { vipDevEnvCreate, vipDevEnvUpdate } from './helpers/commands'; @@ -153,6 +154,53 @@ describe( 'vip dev-env configuration file', () => { return expect( checkEnvExists( expectedSlug ) ).resolves.toBe( true ); } ); + it( 'should create a new environment using parent directory configuration with relative paths', async () => { + // Verify that relative paths are resolved correctly when used in a configuration file in a parent directory + const workingDirectoryPath = await mkdtemp( path.join( os.tmpdir(), 'vip-dev-env-working-' ) ); + const workingDirectoryGrandchild = path.join( workingDirectoryPath, 'child', 'grand-child' ); + await mkdir( workingDirectoryGrandchild, { recursive: true } ); + + const expectedSlug = getProjectSlug(); + expect( await checkEnvExists( expectedSlug ) ).toBe( false ); + + // Write configuration file in top working directory + await writeConfigurationFile( workingDirectoryPath, { + 'configuration-version': '0.preview-unstable', + slug: expectedSlug, + 'app-code': './', + } ); + + await makeRequiredAppCodeDirectories( workingDirectoryPath ); + + // Spawn in grandchild directory + const spawnOptions = { + env, + cwd: workingDirectoryGrandchild, + }; + + const result = await cliTest.spawn( + [ process.argv[ 0 ], vipDevEnvCreate ], + spawnOptions, + true + ); + expect( result.rc ).toBe( 0 ); + expect( result.stdout ).toContain( `Using environment ${ expectedSlug }` ); + expect( result.stderr ).toBe( '' ); + await expect( checkEnvExists( expectedSlug ) ).resolves.toBe( true ); + + // Verify that the app-code directory is pointing to a path relative to the configuration file, + // not the CLI working directory + const environmentData = readEnvironmentData( expectedSlug ); + const localPathResolved = await realpath( workingDirectoryPath ); + + expect( environmentData ).toMatchObject( { + appCode: expect.objectContaining( { + mode: 'local', + dir: localPathResolved, + } ), + } ); + } ); + it( 'should create a new environment with full configuration from file', async () => { const expectedSlug = getProjectSlug(); const expectedTitle = 'Test'; diff --git a/__tests__/devenv-e2e/helpers/utils.js b/__tests__/devenv-e2e/helpers/utils.js index 6f5b7ba61..d5cdc4ab7 100644 --- a/__tests__/devenv-e2e/helpers/utils.js +++ b/__tests__/devenv-e2e/helpers/utils.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { writeFile } from 'node:fs/promises'; +import { writeFile, mkdir } from 'node:fs/promises'; import path from 'node:path'; import { expect } from '@jest/globals'; import { dump } from 'js-yaml'; @@ -111,3 +111,25 @@ export async function writeConfigurationFile( workingDirectoryPath, configuratio 'utf8' ); } + +/** + * Add required appCode directories for environment mapping. + * + * @param {string} workingDirectoryPath Path that represents the root of a VIP application + */ +export async function makeRequiredAppCodeDirectories( workingDirectoryPath ) { + // Add folders to root project so that 'appCode' option verifies + const appCodeDirectories = [ + 'languages', + 'plugins', + 'themes', + 'private', + 'images', + 'client-mu-plugins', + 'vip-config', + ]; + + return Promise.all( + appCodeDirectories.map( dir => mkdir( path.join( workingDirectoryPath, dir ) ) ) + ); +} From e6c544eccda9cc27acd2f9e0ed7487b8a14a497e Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 31 Jul 2023 11:50:46 -0600 Subject: [PATCH 05/15] Adjust relative paths, store configuration file in meta section --- .../dev-environment/dev-environment-cli.ts | 2 +- .../dev-environment-configuration-file.ts | 36 ++++++++++++++++--- src/lib/dev-environment/types.ts | 7 ++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/lib/dev-environment/dev-environment-cli.ts b/src/lib/dev-environment/dev-environment-cli.ts index 46bccbc43..11948fdee 100644 --- a/src/lib/dev-environment/dev-environment-cli.ts +++ b/src/lib/dev-environment/dev-environment-cli.ts @@ -217,7 +217,7 @@ export async function getEnvironmentName( options: EnvironmentNameOptions ): Pro const slug = configurationFileOptions.slug; console.log( `Using environment ${ chalk.blue.bold( slug ) } from ${ chalk.gray( - configurationFileOptions[ 'configuration-path' ] + configurationFileOptions.meta[ 'configuration-path' ] ) }\n` ); diff --git a/src/lib/dev-environment/dev-environment-configuration-file.ts b/src/lib/dev-environment/dev-environment-configuration-file.ts index 82ad2a872..48a3b884d 100644 --- a/src/lib/dev-environment/dev-environment-configuration-file.ts +++ b/src/lib/dev-environment/dev-environment-configuration-file.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { access, constants, readFile } from 'node:fs/promises'; +import { access, constants, readFile, realpath } from 'node:fs/promises'; import path from 'node:path'; import debugLib from 'debug'; import chalk from 'chalk'; @@ -43,8 +43,10 @@ export async function getConfigurationFileOptions(): Promise< ConfigurationFileO } try { - const configuration = sanitizeConfiguration( configurationFromFile ); - configuration[ 'configuration-path' ] = configurationFilePath; + let configuration = sanitizeConfiguration( configurationFromFile ); + configuration = adjustRelativePaths( configuration, configurationFilePath ); + + configuration.meta[ 'configuration-path' ] = configurationFilePath; debug( 'Sanitized configuration from file:', configuration ); return configuration; @@ -115,6 +117,7 @@ function sanitizeConfiguration( mailpit: stringToBooleanIfDefined( configuration.mailpit ?? configuration.mailhog ), 'media-redirect-domain': configuration[ 'media-redirect-domain' ]?.toString(), photon: stringToBooleanIfDefined( configuration.photon ), + meta: {}, }; // Remove undefined values @@ -126,6 +129,22 @@ function sanitizeConfiguration( return sanitizedConfiguration; } +function adjustRelativePaths( + configuration: ConfigurationFileOptions, + configurationFilePath: string +): ConfigurationFileOptions { + const configurationDirectory = path.resolve( path.dirname( configurationFilePath ) ); + const configurationKeysWithRelativePaths = [ 'app-code', 'mu-plugins' ]; + + configurationKeysWithRelativePaths.forEach( key => { + if ( configuration[ key ] && configuration[ key ] !== 'image' ) { + configuration[ key ] = path.join( configurationDirectory, configuration[ key ] ); + } + } ); + + return configuration; +} + export function mergeConfigurationFileOptions( preselectedOptions: InstanceOptions, configurationFileOptions: ConfigurationFileOptions @@ -171,10 +190,17 @@ export function printConfigurationFile( configurationOptions: ConfigurationFileO return; } + // Ignore meta key for configuration path + const ignoreKeys = [ 'meta' ]; + // Customized formatter because Lando's printTable() automatically uppercases keys // which may be confusing for YAML configuration const settingLines = []; for ( const [ key, value ] of Object.entries( configurationOptions ) ) { + if ( ignoreKeys.includes( key ) ) { + continue; + } + settingLines.push( `${ chalk.cyan( key ) }: ${ String( value ) }` ); } @@ -228,8 +254,8 @@ function getConfigurationFileExample(): string { return `configuration-version: ${ getLatestConfigurationFileVersion() } slug: dev-site php: 8.0 -wordpress: 6.0 -app-code: ./site-code +wordpress: 6.2 +app-code: ../ mu-plugins: image multisite: false phpmyadmin: true diff --git a/src/lib/dev-environment/types.ts b/src/lib/dev-environment/types.ts index 80d6bcfe3..a9a2a4911 100644 --- a/src/lib/dev-environment/types.ts +++ b/src/lib/dev-environment/types.ts @@ -74,11 +74,14 @@ export interface ConfigurationFileOptions { 'media-redirect-domain'?: string; photon?: boolean; - 'configuration-path'?: string; - + meta?: ConfigurationFileMeta; [ index: string ]: unknown; } +export interface ConfigurationFileMeta { + 'configuration-path'?: string; +} + export interface InstanceData { [ index: string ]: unknown; siteSlug: string; From 11790297386b6b6810d954189ed7ac940713a867 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 31 Jul 2023 15:11:52 -0600 Subject: [PATCH 06/15] Use .wpvip configuration folder, update tests --- .../devenv-e2e/013-configuration-file.spec.js | 11 ++++--- __tests__/devenv-e2e/helpers/utils.js | 7 +++- .../dev-environment/dev-environment-cli.ts | 1 + .../dev-environment-configuration-file.ts | 32 ++++++++++++------- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/__tests__/devenv-e2e/013-configuration-file.spec.js b/__tests__/devenv-e2e/013-configuration-file.spec.js index 21dd382e6..4e69af6b8 100644 --- a/__tests__/devenv-e2e/013-configuration-file.spec.js +++ b/__tests__/devenv-e2e/013-configuration-file.spec.js @@ -66,8 +66,8 @@ describe( 'vip dev-env configuration file', () => { false ); expect( result.rc ).toBeGreaterThan( 0 ); - expect( result.stderr ).toContain( - "Configuration file .vip-dev-env.yml is available but couldn't be loaded" + expect( result.stderr ).toMatch( + /Configuration file .+\/\.wpvip\/vip-dev-env\.yml is available but couldn't be loaded/ ); return expect( checkEnvExists( slug ) ).resolves.toBe( false ); } ); @@ -90,8 +90,8 @@ describe( 'vip dev-env configuration file', () => { false ); expect( result.rc ).toBeGreaterThan( 0 ); - expect( result.stderr ).toContain( - "Configuration file .vip-dev-env.yml is available but couldn't be loaded" + expect( result.stderr ).toMatch( + /Configuration file .+\/\.wpvip\/vip-dev-env\.yml is available but couldn't be loaded/ ); return expect( checkEnvExists( slug ) ).resolves.toBe( false ); } ); @@ -167,7 +167,7 @@ describe( 'vip dev-env configuration file', () => { await writeConfigurationFile( workingDirectoryPath, { 'configuration-version': '0.preview-unstable', slug: expectedSlug, - 'app-code': './', + 'app-code': '../', } ); await makeRequiredAppCodeDirectories( workingDirectoryPath ); @@ -183,6 +183,7 @@ describe( 'vip dev-env configuration file', () => { spawnOptions, true ); + expect( result.rc ).toBe( 0 ); expect( result.stdout ).toContain( `Using environment ${ expectedSlug }` ); expect( result.stderr ).toBe( '' ); diff --git a/__tests__/devenv-e2e/helpers/utils.js b/__tests__/devenv-e2e/helpers/utils.js index d5cdc4ab7..78d9070eb 100644 --- a/__tests__/devenv-e2e/helpers/utils.js +++ b/__tests__/devenv-e2e/helpers/utils.js @@ -14,6 +14,8 @@ import { getEnvironmentPath, } from '../../../src/lib/dev-environment/dev-environment-core'; import { vipDevEnvCreate, vipDevEnvDestroy, vipDevEnvStart } from './commands'; +import { CONFIGURATION_FOLDER } from '../../../src/lib/dev-environment/dev-environment-cli'; +import { CONFIGURATION_FILE_NAME } from '../../../src/lib/dev-environment/dev-environment-configuration-file'; let id = 0; @@ -104,9 +106,12 @@ export async function destroyEnvironment( cliTest, slug, env ) { * @param {Object} configuration Configuration file values */ export async function writeConfigurationFile( workingDirectoryPath, configuration ) { + const configurationDirectoryPath = path.join( workingDirectoryPath, CONFIGURATION_FOLDER ); + await mkdir( configurationDirectoryPath, { recursive: true } ); + const configurationLines = dump( configuration ); await writeFile( - path.join( workingDirectoryPath, '.vip-dev-env.yml' ), + path.join( configurationDirectoryPath, CONFIGURATION_FILE_NAME ), configurationLines, 'utf8' ); diff --git a/src/lib/dev-environment/dev-environment-cli.ts b/src/lib/dev-environment/dev-environment-cli.ts index 11948fdee..ef8d3acbb 100644 --- a/src/lib/dev-environment/dev-environment-cli.ts +++ b/src/lib/dev-environment/dev-environment-cli.ts @@ -56,6 +56,7 @@ import { Args } from '../cli/command'; const debug = debugLib( '@automattic/vip:bin:dev-environment' ); export const DEFAULT_SLUG = 'vip-local'; +export const CONFIGURATION_FOLDER = '.wpvip'; let isStdinTTY: boolean = Boolean( process.stdin.isTTY ); diff --git a/src/lib/dev-environment/dev-environment-configuration-file.ts b/src/lib/dev-environment/dev-environment-configuration-file.ts index 48a3b884d..55b65c17c 100644 --- a/src/lib/dev-environment/dev-environment-configuration-file.ts +++ b/src/lib/dev-environment/dev-environment-configuration-file.ts @@ -10,12 +10,13 @@ import yaml, { FAILSAFE_SCHEMA } from 'js-yaml'; /** * Internal dependencies */ +import { CONFIGURATION_FOLDER } from './dev-environment-cli'; import * as exit from '../cli/exit'; -import type { ConfigurationFileOptions, InstanceOptions } from './types'; +import type { ConfigurationFileMeta, ConfigurationFileOptions, InstanceOptions } from './types'; const debug = debugLib( '@automattic/vip:bin:dev-environment' ); -export const CONFIGURATION_FILE_NAME = '.vip-dev-env.yml'; +export const CONFIGURATION_FILE_NAME = 'vip-dev-env.yml'; export async function getConfigurationFileOptions(): Promise< ConfigurationFileOptions > { const configurationFilePath = await findConfigurationFilePath(); @@ -37,17 +38,15 @@ export async function getConfigurationFileOptions(): Promise< ConfigurationFileO } ) as Record< string, unknown >; } catch ( err ) { const messageToShow = - `Configuration file ${ chalk.grey( CONFIGURATION_FILE_NAME ) } could not be loaded:\n` + + `Configuration file ${ chalk.grey( configurationFilePath ) } could not be loaded:\n` + ( err as Error ).toString(); exit.withError( messageToShow ); } try { - let configuration = sanitizeConfiguration( configurationFromFile ); + let configuration = sanitizeConfiguration( configurationFromFile, configurationFilePath ); configuration = adjustRelativePaths( configuration, configurationFilePath ); - configuration.meta[ 'configuration-path' ] = configurationFilePath; - debug( 'Sanitized configuration from file:', configuration ); return configuration; } catch ( err ) { @@ -56,10 +55,11 @@ export async function getConfigurationFileOptions(): Promise< ConfigurationFileO } function sanitizeConfiguration( - configuration: Record< string, unknown > + configuration: Record< string, unknown >, + configurationFilePath: string ): ConfigurationFileOptions { const genericConfigurationError = - `Configuration file ${ chalk.grey( CONFIGURATION_FILE_NAME ) } is available but ` + + `Configuration file ${ chalk.grey( configurationFilePath ) } is available but ` + `couldn't be loaded. Ensure there is a ${ chalk.cyan( 'configuration-version' ) } and ${ chalk.cyan( 'slug' ) } ` + @@ -85,7 +85,7 @@ function sanitizeConfiguration( if ( ! isValidConfigurationFileVersion( version.toString() ) ) { throw new Error( - `Configuration file ${ chalk.grey( CONFIGURATION_FILE_NAME ) } has an invalid ` + + `Configuration file ${ chalk.grey( configurationFilePath ) } has an invalid ` + `${ chalk.cyan( 'configuration-version' ) } key. Update to a supported version. For example:\n\n` + @@ -101,6 +101,10 @@ function sanitizeConfiguration( return value === 'true'; }; + const configurationMeta: ConfigurationFileMeta = { + 'configuration-path': configurationFilePath, + }; + const sanitizedConfiguration: ConfigurationFileOptions = { version: version.toString(), // eslint-disable-next-line @typescript-eslint/no-base-to-string @@ -117,7 +121,7 @@ function sanitizeConfiguration( mailpit: stringToBooleanIfDefined( configuration.mailpit ?? configuration.mailhog ), 'media-redirect-domain': configuration[ 'media-redirect-domain' ]?.toString(), photon: stringToBooleanIfDefined( configuration.photon ), - meta: {}, + meta: configurationMeta, }; // Remove undefined values @@ -212,11 +216,15 @@ async function findConfigurationFilePath(): Promise< string | false > { const rootPath = path.parse( currentPath ).root; let depth = 0; - const maxDepth = 32; + const maxDepth = 64; const pathPromises = []; while ( currentPath !== rootPath && depth < maxDepth ) { - const configurationFilePath = path.join( currentPath, CONFIGURATION_FILE_NAME ); + const configurationFilePath = path.join( + currentPath, + CONFIGURATION_FOLDER, + CONFIGURATION_FILE_NAME + ); pathPromises.push( access( configurationFilePath, constants.R_OK ).then( () => configurationFilePath ) From e73358aa1a47d074a161f1cd015a03903dd204d6 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 31 Jul 2023 15:26:09 -0600 Subject: [PATCH 07/15] Remove `printConfigurationFile()` as it's redundant and loud --- src/bin/vip-dev-env-create.js | 4 +-- .../dev-environment-configuration-file.ts | 35 ++++--------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/bin/vip-dev-env-create.js b/src/bin/vip-dev-env-create.js index a9ea0aedb..597ba253c 100755 --- a/src/bin/vip-dev-env-create.js +++ b/src/bin/vip-dev-env-create.js @@ -42,7 +42,6 @@ import { } from '../lib/constants/dev-environment'; import { getConfigurationFileOptions, - printConfigurationFile, mergeConfigurationFileOptions, } from '../lib/dev-environment/dev-environment-configuration-file'; import type { InstanceOptions } from '../lib/dev-environment/types'; @@ -149,8 +148,7 @@ cmd.argv( process.argv, async ( arg, opt ) => { let suppressPrompts = false; if ( Object.keys( configurationFileOptions ).length > 0 ) { - console.log( '\nUsing configuration from file.' ); - printConfigurationFile( configurationFileOptions ); + // Merge configuration from file preselectedOptions = mergeConfigurationFileOptions( opt, configurationFileOptions ); suppressPrompts = true; } diff --git a/src/lib/dev-environment/dev-environment-configuration-file.ts b/src/lib/dev-environment/dev-environment-configuration-file.ts index 55b65c17c..601b2e341 100644 --- a/src/lib/dev-environment/dev-environment-configuration-file.ts +++ b/src/lib/dev-environment/dev-environment-configuration-file.ts @@ -187,30 +187,6 @@ export function mergeConfigurationFileOptions( return mergedOptions; } -export function printConfigurationFile( configurationOptions: ConfigurationFileOptions ) { - const isConfigurationFileEmpty = Object.keys( configurationOptions ).length === 0; - - if ( isConfigurationFileEmpty ) { - return; - } - - // Ignore meta key for configuration path - const ignoreKeys = [ 'meta' ]; - - // Customized formatter because Lando's printTable() automatically uppercases keys - // which may be confusing for YAML configuration - const settingLines = []; - for ( const [ key, value ] of Object.entries( configurationOptions ) ) { - if ( ignoreKeys.includes( key ) ) { - continue; - } - - settingLines.push( `${ chalk.cyan( key ) }: ${ String( value ) }` ); - } - - console.log( settingLines.join( '\n' ) + '\n' ); -} - async function findConfigurationFilePath(): Promise< string | false > { let currentPath = process.cwd(); const rootPath = path.parse( currentPath ).root; @@ -244,7 +220,7 @@ async function findConfigurationFilePath(): Promise< string | false > { .catch( () => false ); } -const CONFIGURATION_FILE_VERSIONS = [ '0.preview-unstable' ]; +const CONFIGURATION_FILE_VERSIONS = [ '1' ]; function getAllConfigurationFileVersions(): string[] { return CONFIGURATION_FILE_VERSIONS; @@ -261,13 +237,16 @@ function isValidConfigurationFileVersion( version: string ): boolean { function getConfigurationFileExample(): string { return `configuration-version: ${ getLatestConfigurationFileVersion() } slug: dev-site +title: Dev Site php: 8.0 wordpress: 6.2 app-code: ../ mu-plugins: image multisite: false -phpmyadmin: true -elasticsearch: true -xdebug: true +phpmyadmin: false +elasticsearch: false +xdebug: false +mailpit: false +photon: false `; } From e0c7acf15587759eefd00a6e23e7426dcf5b185c Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 31 Jul 2023 15:33:40 -0600 Subject: [PATCH 08/15] Update configuration-version in tests --- .../devenv-e2e/013-configuration-file.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/__tests__/devenv-e2e/013-configuration-file.spec.js b/__tests__/devenv-e2e/013-configuration-file.spec.js index 4e69af6b8..6044f07e6 100644 --- a/__tests__/devenv-e2e/013-configuration-file.spec.js +++ b/__tests__/devenv-e2e/013-configuration-file.spec.js @@ -77,7 +77,7 @@ describe( 'vip dev-env configuration file', () => { expect( await checkEnvExists( slug ) ).toBe( false ); await writeConfigurationFile( tmpWorkingDirectoryPath, { - 'configuration-version': '0.preview-unstable', + 'configuration-version': '1', } ); const spawnOptions = { @@ -101,7 +101,7 @@ describe( 'vip dev-env configuration file', () => { expect( await checkEnvExists( expectedSlug ) ).toBe( false ); await writeConfigurationFile( tmpWorkingDirectoryPath, { - 'configuration-version': '0.preview-unstable', + 'configuration-version': '1', slug: expectedSlug, } ); @@ -132,7 +132,7 @@ describe( 'vip dev-env configuration file', () => { // Write configuration file in top working directory await writeConfigurationFile( workingDirectoryPath, { - 'configuration-version': '0.preview-unstable', + 'configuration-version': '1', slug: expectedSlug, } ); @@ -165,7 +165,7 @@ describe( 'vip dev-env configuration file', () => { // Write configuration file in top working directory await writeConfigurationFile( workingDirectoryPath, { - 'configuration-version': '0.preview-unstable', + 'configuration-version': '1', slug: expectedSlug, 'app-code': '../', } ); @@ -217,7 +217,7 @@ describe( 'vip dev-env configuration file', () => { expect( await checkEnvExists( expectedSlug ) ).toBe( false ); await writeConfigurationFile( tmpWorkingDirectoryPath, { - 'configuration-version': '0.preview-unstable', + 'configuration-version': '1', slug: expectedSlug, title: expectedTitle, multisite: expectedMultisite, @@ -279,7 +279,7 @@ describe( 'vip dev-env configuration file', () => { // Setup initial environment await writeConfigurationFile( tmpWorkingDirectoryPath, { - 'configuration-version': '0.preview-unstable', + 'configuration-version': '1', slug, elasticsearch: expectedElasticsearch, phpmyadmin: expectedPhpMyAdmin, @@ -309,7 +309,7 @@ describe( 'vip dev-env configuration file', () => { // Update environment from changed configuration file await writeConfigurationFile( tmpWorkingDirectoryPath, { - 'configuration-version': '0.preview-unstable', + 'configuration-version': '1', slug, elasticsearch: ! expectedElasticsearch, phpmyadmin: ! expectedPhpMyAdmin, From f7859feeca4918f292f5b44be33495e72ccccc49 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Wed, 2 Aug 2023 16:05:59 -0600 Subject: [PATCH 09/15] Fix meta type and string mapping type errors --- src/lib/dev-environment/dev-environment-cli.ts | 2 +- .../dev-environment-configuration-file.ts | 16 ++++++++++------ src/lib/dev-environment/types.ts | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/lib/dev-environment/dev-environment-cli.ts b/src/lib/dev-environment/dev-environment-cli.ts index ef8d3acbb..766853d37 100644 --- a/src/lib/dev-environment/dev-environment-cli.ts +++ b/src/lib/dev-environment/dev-environment-cli.ts @@ -214,7 +214,7 @@ export async function getEnvironmentName( options: EnvironmentNameOptions ): Pro const configurationFileOptions = await getConfigurationFileOptions(); - if ( configurationFileOptions.slug ) { + if ( configurationFileOptions.slug && configurationFileOptions.meta ) { const slug = configurationFileOptions.slug; console.log( `Using environment ${ chalk.blue.bold( slug ) } from ${ chalk.gray( diff --git a/src/lib/dev-environment/dev-environment-configuration-file.ts b/src/lib/dev-environment/dev-environment-configuration-file.ts index da77ffd23..f292ffb01 100644 --- a/src/lib/dev-environment/dev-environment-configuration-file.ts +++ b/src/lib/dev-environment/dev-environment-configuration-file.ts @@ -139,13 +139,17 @@ function adjustRelativePaths( configurationFilePath: string ): ConfigurationFileOptions { const configurationDirectory = path.resolve( path.dirname( configurationFilePath ) ); - const configurationKeysWithRelativePaths = [ 'app-code', 'mu-plugins' ]; - configurationKeysWithRelativePaths.forEach( key => { - if ( configuration[ key ] && configuration[ key ] !== 'image' ) { - configuration[ key ] = path.join( configurationDirectory, configuration[ key ] ); - } - } ); + if ( configuration[ 'app-code' ] && configuration[ 'app-code' ] !== 'image' ) { + configuration[ 'app-code' ] = path.join( configurationDirectory, configuration[ 'app-code' ] ); + } + + if ( configuration[ 'mu-plugins' ] && configuration[ 'mu-plugins' ] !== 'image' ) { + configuration[ 'mu-plugins' ] = path.join( + configurationDirectory, + configuration[ 'mu-plugins' ] + ); + } return configuration; } diff --git a/src/lib/dev-environment/types.ts b/src/lib/dev-environment/types.ts index a9a2a4911..6bc3b0df2 100644 --- a/src/lib/dev-environment/types.ts +++ b/src/lib/dev-environment/types.ts @@ -79,7 +79,7 @@ export interface ConfigurationFileOptions { } export interface ConfigurationFileMeta { - 'configuration-path'?: string; + 'configuration-path': string; } export interface InstanceData { From cfb82295b9339b58c2dc1ff5e2aafd59d57a4edd Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Wed, 2 Aug 2023 16:29:45 -0600 Subject: [PATCH 10/15] Fix unrelated type issue --- src/lib/constants/dev-environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/constants/dev-environment.ts b/src/lib/constants/dev-environment.ts index 02534f07a..4c56966cf 100644 --- a/src/lib/constants/dev-environment.ts +++ b/src/lib/constants/dev-environment.ts @@ -28,7 +28,7 @@ export const DEV_ENVIRONMENT_WORDPRESS_CACHE_KEY = 'wordpress-versions.json'; export const DEV_ENVIRONMENT_WORDPRESS_VERSION_TTL = 86400; // once per day -export const DEV_ENVIRONMENT_PHP_VERSIONS: Record< string, string | undefined > = { +export const DEV_ENVIRONMENT_PHP_VERSIONS: Record< string, string > = { '8.0': 'ghcr.io/automattic/vip-container-images/php-fpm:8.0', 8.2: 'ghcr.io/automattic/vip-container-images/php-fpm:8.2', 8.1: 'ghcr.io/automattic/vip-container-images/php-fpm:8.1', From d79138c03763fad943ce5ea5f747b0315dbcbd66 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Wed, 2 Aug 2023 16:43:58 -0600 Subject: [PATCH 11/15] Fix configuration file mocking --- __tests__/lib/dev-environment/dev-environment-cli.js | 1 + 1 file changed, 1 insertion(+) diff --git a/__tests__/lib/dev-environment/dev-environment-cli.js b/__tests__/lib/dev-environment/dev-environment-cli.js index 6b69aa871..ee90b8242 100644 --- a/__tests__/lib/dev-environment/dev-environment-cli.js +++ b/__tests__/lib/dev-environment/dev-environment-cli.js @@ -185,6 +185,7 @@ describe( 'lib/dev-environment/dev-environment-cli', () => { ); getConfigurationFileOptionsMock.mockReturnValue( { slug: 'config-file-slug', + meta: {}, } ); } ); From 3d894106b95044f263be5fc377d9c9a7e4c4b1ac Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 16 Oct 2023 13:38:25 -0600 Subject: [PATCH 12/15] Fix missing imports --- src/lib/dev-environment/dev-environment-cli.ts | 7 ++----- .../dev-environment/dev-environment-configuration-file.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/lib/dev-environment/dev-environment-cli.ts b/src/lib/dev-environment/dev-environment-cli.ts index 1ee7029af..7a64f363d 100644 --- a/src/lib/dev-environment/dev-environment-cli.ts +++ b/src/lib/dev-environment/dev-environment-cli.ts @@ -47,10 +47,7 @@ import type { } from './types'; import { validateDockerInstalled, validateDockerAccess } from './dev-environment-lando'; import UserError from '../user-error'; -import { - CONFIGURATION_FILE_NAME, - getConfigurationFileOptions, -} from './dev-environment-configuration-file'; +import { getConfigurationFileOptions } from './dev-environment-configuration-file'; import { Args } from '../cli/command'; const debug = debugLib( '@automattic/vip:bin:dev-environment' ); @@ -688,7 +685,7 @@ export function resolvePhpVersion( version: string ): string { result = images[ 0 ]; } } else { - result = DEV_ENVIRONMENT_PHP_VERSIONS[ version ] as string; + result = DEV_ENVIRONMENT_PHP_VERSIONS[ version ]!; } debug( 'Resolved PHP image: %j', result ); diff --git a/src/lib/dev-environment/dev-environment-configuration-file.ts b/src/lib/dev-environment/dev-environment-configuration-file.ts index f292ffb01..beb8242d0 100644 --- a/src/lib/dev-environment/dev-environment-configuration-file.ts +++ b/src/lib/dev-environment/dev-environment-configuration-file.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { access, readFile, realpath } from 'node:fs/promises'; +import { access, readFile } from 'node:fs/promises'; import { constants } from 'fs'; import path from 'node:path'; import debugLib from 'debug'; From c521af81d59483f4c4cbaa3d0155493baf20a698 Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 8 Jan 2024 12:19:46 -0700 Subject: [PATCH 13/15] Fix import linting errors --- __tests__/devenv-e2e/helpers/utils.js | 4 ++-- src/lib/dev-environment/dev-environment-configuration-file.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/__tests__/devenv-e2e/helpers/utils.js b/__tests__/devenv-e2e/helpers/utils.js index b66cf7777..67e8219fb 100644 --- a/__tests__/devenv-e2e/helpers/utils.js +++ b/__tests__/devenv-e2e/helpers/utils.js @@ -4,12 +4,12 @@ import { writeFile, mkdir } from 'node:fs/promises'; import path from 'node:path'; import { vipDevEnvCreate, vipDevEnvDestroy, vipDevEnvStart } from './commands'; +import { CONFIGURATION_FOLDER } from '../../../src/lib/dev-environment/dev-environment-cli'; +import { CONFIGURATION_FILE_NAME } from '../../../src/lib/dev-environment/dev-environment-configuration-file'; import { doesEnvironmentExist, getEnvironmentPath, } from '../../../src/lib/dev-environment/dev-environment-core'; -import { CONFIGURATION_FOLDER } from '../../../src/lib/dev-environment/dev-environment-cli'; -import { CONFIGURATION_FILE_NAME } from '../../../src/lib/dev-environment/dev-environment-configuration-file'; let id = 0; diff --git a/src/lib/dev-environment/dev-environment-configuration-file.ts b/src/lib/dev-environment/dev-environment-configuration-file.ts index 8625d908e..3e0685271 100644 --- a/src/lib/dev-environment/dev-environment-configuration-file.ts +++ b/src/lib/dev-environment/dev-environment-configuration-file.ts @@ -5,9 +5,10 @@ import yaml, { FAILSAFE_SCHEMA } from 'js-yaml'; import { access, readFile } from 'node:fs/promises'; import path from 'node:path'; +import { CONFIGURATION_FOLDER } from './dev-environment-cli'; import * as exit from '../cli/exit'; + import type { ConfigurationFileMeta, ConfigurationFileOptions, InstanceOptions } from './types'; -import { CONFIGURATION_FOLDER } from './dev-environment-cli'; const debug = debugLib( '@automattic/vip:bin:dev-environment' ); From bbc2f50be79e78c2cd2ad604db042080081ff1bd Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 8 Jan 2024 12:20:09 -0700 Subject: [PATCH 14/15] Disable correct rule for cancelCommand to pass linting --- src/bin/vip-wp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/vip-wp.js b/src/bin/vip-wp.js index 9c95ae67c..0ff75efda 100755 --- a/src/bin/vip-wp.js +++ b/src/bin/vip-wp.js @@ -107,7 +107,7 @@ const getTokenForCommand = async ( appId, envId, command ) => { } ); }; -// eslint-disable-next-line no-unused-vars +// eslint-disable-next-line @typescript-eslint/no-unused-vars const cancelCommand = async guid => { const api = await API(); return api.mutate( { From 631f443344c96dea4aba896e139a061774bb307e Mon Sep 17 00:00:00 2001 From: Alec Geatches Date: Mon, 22 Jan 2024 11:32:21 -0700 Subject: [PATCH 15/15] Move configuration file loading to findConfigurationFile() --- .../dev-environment-configuration-file.ts | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/lib/dev-environment/dev-environment-configuration-file.ts b/src/lib/dev-environment/dev-environment-configuration-file.ts index 6b6c3785c..1eac0dea5 100644 --- a/src/lib/dev-environment/dev-environment-configuration-file.ts +++ b/src/lib/dev-environment/dev-environment-configuration-file.ts @@ -14,43 +14,32 @@ const debug = debugLib( '@automattic/vip:bin:dev-environment' ); export const CONFIGURATION_FILE_NAME = 'vip-dev-env.yml'; export async function getConfigurationFileOptions(): Promise< ConfigurationFileOptions > { - const configurationFilePath = await findConfigurationFilePath(); + const configurationFile = await findConfigurationFile(); - if ( configurationFilePath === false ) { + if ( configurationFile === false ) { return {}; } - let configurationFileContents; - - try { - configurationFileContents = await readFile( configurationFilePath, 'utf8' ); - debug( 'Read configuration file from %s', configurationFilePath ); - } catch ( err ) { - if ( ( err as NodeJS.ErrnoException ).code === 'ENOENT' ) { - return {}; - } - - throw err; - } + const { configurationPath, configurationContents } = configurationFile; let configurationFromFile: Record< string, unknown > = {}; try { - configurationFromFile = yaml.load( configurationFileContents, { + configurationFromFile = yaml.load( configurationContents, { // Only allow strings, arrays, and objects to be parsed from configuration file // This causes number-looking values like `php: 8.1` to be parsed directly into strings schema: FAILSAFE_SCHEMA, } ) as Record< string, unknown >; } catch ( err ) { const messageToShow = - `Configuration file ${ chalk.grey( configurationFilePath ) } could not be loaded:\n` + + `Configuration file ${ chalk.grey( configurationPath ) } could not be loaded:\n` + ( err as Error ).toString(); exit.withError( messageToShow ); } try { - let configuration = sanitizeConfiguration( configurationFromFile, configurationFilePath ); - configuration = adjustRelativePaths( configuration, configurationFilePath ); + let configuration = sanitizeConfiguration( configurationFromFile, configurationPath ); + configuration = adjustRelativePaths( configuration, configurationPath ); debug( 'Sanitized configuration from file:', configuration ); return configuration; @@ -196,7 +185,9 @@ export function mergeConfigurationFileOptions( return mergedOptions; } -async function findConfigurationFilePath(): Promise< string | false > { +async function findConfigurationFile(): Promise< + { configurationPath: string; configurationContents: string } | false +> { let currentPath = process.cwd(); const rootPath = path.parse( currentPath ).root; @@ -205,14 +196,17 @@ async function findConfigurationFilePath(): Promise< string | false > { const pathPromises = []; while ( currentPath !== rootPath && depth < maxDepth ) { - const configurationFilePath = path.join( + const configurationPath = path.join( currentPath, CONFIGURATION_FOLDER, CONFIGURATION_FILE_NAME ); pathPromises.push( - access( configurationFilePath, constants.R_OK ).then( () => configurationFilePath ) + readFile( configurationPath, 'utf8' ).then( configurationContents => ( { + configurationPath, + configurationContents, + } ) ) ); // Move up one directory @@ -222,11 +216,7 @@ async function findConfigurationFilePath(): Promise< string | false > { depth++; } - return Promise.any( pathPromises ) - .then( configurationFilePath => { - return configurationFilePath; - } ) - .catch( () => false ); + return Promise.any( pathPromises ).catch( () => false ); } const CONFIGURATION_FILE_VERSIONS = [ '1' ];