diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dee57f56eae..fe866834755e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Vis Builder] Change classname prefix wiz to vb ([#2581](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2581/files)) - [Vis Builder] Change wizard to vis_builder in file names and paths ([#2587](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2587)) - [Windows] Facilitate building and running OSD and plugins on Windows platforms ([#2601](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2601)) +- [Windows] Add helper functions to work around the differences of platforms ([#2703](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2703)) - [Multi DataSource] Address UX comments on Data source list and create page ([#2625](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2625)) - [Vis Builder] Rename wizard to visBuilder in i18n id and formatted message id ([#2635](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2635)) - [Vis Builder] Rename wizard to visBuilder in class name, type name and function name ([#2639](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2639)) diff --git a/package.json b/package.json index 969ba11831e6..437412617326 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "@osd/apm-config-loader": "1.0.0", "@osd/config": "1.0.0", "@osd/config-schema": "1.0.0", + "@osd/cross-platform": "1.0.0", "@osd/i18n": "1.0.0", "@osd/interpreter": "1.0.0", "@osd/logging": "1.0.0", diff --git a/packages/osd-config-schema/package.json b/packages/osd-config-schema/package.json index 1e07c42cba5a..52471e29527c 100644 --- a/packages/osd-config-schema/package.json +++ b/packages/osd-config-schema/package.json @@ -10,8 +10,9 @@ "osd:bootstrap": "yarn build" }, "devDependencies": { - "typescript": "4.0.2", - "tsd": "^0.21.0" + "@osd/cross-platform": "1.0.0", + "tsd": "^0.21.0", + "typescript": "4.0.2" }, "peerDependencies": { "lodash": "^4.17.21", diff --git a/packages/osd-config-schema/src/errors/schema_error.test.ts b/packages/osd-config-schema/src/errors/schema_error.test.ts index 9e7b5a897081..c134c888d510 100644 --- a/packages/osd-config-schema/src/errors/schema_error.test.ts +++ b/packages/osd-config-schema/src/errors/schema_error.test.ts @@ -31,6 +31,8 @@ import { relative, sep } from 'path'; import { SchemaError } from '.'; +import { standardize, getRepoRoot } from '@osd/cross-platform'; + /** * Make all paths in stacktrace relative. */ @@ -46,9 +48,7 @@ export const cleanStack = (stack: string) => } const path = parts[1]; - // Cannot use `standardize` from `@osd/utils - let relativePath = relative(process.cwd(), path); - if (process.platform === 'win32') relativePath = relativePath.replace(/\\/g, '/'); + const relativePath = standardize(relative(getRepoRoot(path) || '.', path)); return line.replace(path, relativePath); }) diff --git a/packages/osd-cross-platform/README.md b/packages/osd-cross-platform/README.md new file mode 100644 index 000000000000..a61a2f184f02 --- /dev/null +++ b/packages/osd-cross-platform/README.md @@ -0,0 +1,3 @@ +# `@osd/cross-platform` — OpenSearch Dashboards cross-platform helpers + +This package contains the helper functions to work around the differences of platforms diff --git a/packages/osd-cross-platform/package.json b/packages/osd-cross-platform/package.json new file mode 100644 index 000000000000..1af90b00a98f --- /dev/null +++ b/packages/osd-cross-platform/package.json @@ -0,0 +1,15 @@ +{ + "name": "@osd/cross-platform", + "main": "./target/index.js", + "version": "1.0.0", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "tsc", + "osd:bootstrap": "yarn build" + }, + "devDependencies": { + "typescript": "4.0.2", + "tsd": "^0.21.0" + } +} diff --git a/packages/osd-cross-platform/src/__snapshots__/path.test.ts.snap b/packages/osd-cross-platform/src/__snapshots__/path.test.ts.snap new file mode 100644 index 000000000000..20c44ec1c2d6 --- /dev/null +++ b/packages/osd-cross-platform/src/__snapshots__/path.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cross Platform path standardize on POSIX-compatible platforms ignores additional parameters 1`] = `"/a/b/c"`; + +exports[`Cross Platform path standardize on POSIX-compatible platforms produces a path in POSIX format 1`] = `"/a/b/c"`; + +exports[`Cross Platform path standardize on Windows produces a path in POSIX format 1`] = `"C:/a/b/c"`; + +exports[`Cross Platform path standardize on Windows produces a path in native format 1`] = `"C:\\\\a\\\\b\\\\c"`; + +exports[`Cross Platform path standardize on Windows produces a path in native format even for POSIX input 1`] = `"C:\\\\a\\\\b\\\\c"`; + +exports[`Cross Platform path standardize on Windows produces a path in native format with escaped backslashes 1`] = `"C:\\\\\\\\a\\\\\\\\b\\\\\\\\c"`; diff --git a/packages/osd-cross-platform/src/index.ts b/packages/osd-cross-platform/src/index.ts new file mode 100644 index 000000000000..bc05aa9a955d --- /dev/null +++ b/packages/osd-cross-platform/src/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './path'; +export * from './process'; +export * from './repo_root'; diff --git a/packages/osd-cross-platform/src/path.test.ts b/packages/osd-cross-platform/src/path.test.ts new file mode 100644 index 000000000000..ed405298ad13 --- /dev/null +++ b/packages/osd-cross-platform/src/path.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import path from 'path'; +import fs from 'fs'; +import { access, rmdir, mkdir, writeFile, symlink } from 'fs/promises'; + +import { + resolveToFullNameSync, + resolveToFullPathSync, + resolveToShortNameSync, + resolveToShortPathSync, + shortNamesSupportedSync, + realPathSync, + realShortPathSync, + standardize, +} from './path'; + +const tmpTestFolder = './__test_artifacts__'; +const longFolderName = '.long-folder-name'; +const longFileName = '.long-file-name.txt'; +const longSymlinkName = '.sym.link'; +const shortFolderName = 'LONG-F~1'; +const shortFileName = 'LONG-F~1.TXT'; +const dummyWindowsPath = 'C:\\a\\b\\c'; +const dummyWindowsPOSIXPath = 'C:/a/b/c'; +const dummyPOSIXPath = '/a/b/c'; + +const onWindows = process.platform === 'win32' ? describe : xdescribe; +const onWindowsWithShortNames = shortNamesSupportedSync() ? describe : xdescribe; + +// Save the real process.platform +const realPlatform = Object.getOwnPropertyDescriptor(process, 'platform')!; + +describe('Cross Platform', () => { + describe('path', () => { + onWindows('on Windows', () => { + onWindowsWithShortNames('when 8.3 is supported', () => { + beforeAll(async () => { + // Cleanup + try { + // If leftover artifacts were found, get rid of them + await access(tmpTestFolder); + await rmdir(tmpTestFolder, { recursive: true }); + } catch (ex) { + // Do nothing; if `rmdir` failed, let the `mkdir` below throw the error + } + + await mkdir(tmpTestFolder); + await mkdir(path.resolve(tmpTestFolder, longFolderName)); + await writeFile(path.resolve(tmpTestFolder, longFolderName, longFileName), ''); + await symlink( + path.resolve(tmpTestFolder, longFolderName), + path.resolve(tmpTestFolder, longSymlinkName), + 'junction' + ); + }); + + afterAll(async () => { + try { + await rmdir(tmpTestFolder, { recursive: true }); + } catch (ex) { + // Do nothing + } + }); + + it('can synchronously extract full name of a folder', () => { + const name = path.basename( + resolveToFullPathSync(path.resolve(tmpTestFolder, shortFolderName)) + ); + expect(name).toBe(longFolderName); + }); + + it('can synchronously extract full name of a file', () => { + const name = path.basename( + resolveToFullNameSync(path.resolve(tmpTestFolder, shortFolderName, shortFileName)) + ); + expect(name).toBe(longFileName); + }); + + it('can synchronously extract short name of a folder', () => { + const name = path.basename( + resolveToShortPathSync(path.resolve(tmpTestFolder, longFolderName)) + ); + expect(name).toBe(shortFolderName); + }); + + it('can synchronously extract short name of a file', () => { + const name = path.basename( + resolveToShortNameSync(path.resolve(tmpTestFolder, longFolderName, longFileName)) + ); + expect(name).toBe(shortFileName); + }); + + it('can synchronously extract full name of a symbolic link', () => { + const name = path.basename(realPathSync(path.resolve(tmpTestFolder, longSymlinkName))); + expect(name).toBe(longFolderName); + }); + + it('can synchronously extract short name of a symbolic link', () => { + const name = path.basename( + realShortPathSync(path.resolve(tmpTestFolder, longSymlinkName)) + ); + expect(name).toBe(shortFolderName); + }); + }); + }); + + describe('on platforms other than Windows', () => { + let mockPathNormalize: jest.SpyInstance<string, [p: string]>; + let mockPathResolve: jest.SpyInstance<string, string[]>; + let mockFSRealPathSync: jest.SpyInstance<string>; + + beforeAll(() => { + Object.defineProperty(process, 'platform', { + ...Object.getOwnPropertyDescriptor(process, 'property'), + value: 'linux', + }); + + mockPathNormalize = jest.spyOn(path, 'normalize').mockReturnValue(dummyPOSIXPath); + mockPathResolve = jest.spyOn(path, 'resolve').mockReturnValue(dummyPOSIXPath); + mockFSRealPathSync = jest + .spyOn(fs, 'realpathSync') + .mockReturnValue(dummyPOSIXPath) as jest.SpyInstance<string>; + }); + + afterAll(() => { + // Restore the real property value after each test + Object.defineProperty(process, 'platform', realPlatform); + mockPathNormalize.mockRestore(); + mockPathResolve.mockRestore(); + mockFSRealPathSync.mockRestore(); + }); + + it('all short and full name methods return just the normalized paths', () => { + expect(shortNamesSupportedSync()).toBe(false); + expect(resolveToFullPathSync(dummyPOSIXPath)).toBe(dummyPOSIXPath); + expect(resolveToShortPathSync(dummyPOSIXPath)).toBe(dummyPOSIXPath); + }); + }); + + describe('standardize', () => { + describe('on Windows', () => { + let mockPathNormalize: jest.SpyInstance<string, [p: string]>; + + beforeAll(() => { + Object.defineProperty(process, 'platform', { + ...Object.getOwnPropertyDescriptor(process, 'property'), + value: 'win32', + }); + + mockPathNormalize = jest.spyOn(path, 'normalize').mockReturnValue(dummyWindowsPath); + }); + + afterAll(() => { + // Restore the real property value after each test + Object.defineProperty(process, 'platform', realPlatform); + mockPathNormalize.mockRestore(); + }); + + it('produces a path in native format', () => { + expect(standardize(dummyWindowsPath, false, false)).toMatchSnapshot(); + }); + + it('produces a path in native format even for POSIX input', () => { + expect(standardize(dummyWindowsPOSIXPath, false, false)).toMatchSnapshot(); + }); + + it('produces a path in native format with escaped backslashes', () => { + expect(standardize(dummyWindowsPath, false, true)).toMatchSnapshot(); + }); + + it('produces a path in POSIX format', () => { + expect(standardize(dummyWindowsPath)).toMatchSnapshot(); + }); + }); + + describe('on POSIX-compatible platforms', () => { + let mockPathNormalize: jest.SpyInstance<string, [p: string]>; + + beforeAll(() => { + Object.defineProperty(process, 'platform', { + ...Object.getOwnPropertyDescriptor(process, 'property'), + value: 'linux', + }); + + mockPathNormalize = jest.spyOn(path, 'normalize').mockReturnValue(dummyPOSIXPath); + }); + + afterAll(() => { + // Restore the real property value after each test + Object.defineProperty(process, 'platform', realPlatform); + mockPathNormalize.mockRestore(); + }); + + it('produces a path in POSIX format', () => { + expect(standardize(dummyPOSIXPath)).toMatchSnapshot(); + }); + + it('ignores additional parameters', () => { + expect(standardize(dummyPOSIXPath, false, true)).toMatchSnapshot(); + }); + }); + }); + }); +}); diff --git a/packages/osd-cross-platform/src/path.ts b/packages/osd-cross-platform/src/path.ts new file mode 100644 index 000000000000..d5b2befb56c1 --- /dev/null +++ b/packages/osd-cross-platform/src/path.ts @@ -0,0 +1,160 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execSync } from 'child_process'; +import { basename, normalize, resolve } from 'path'; +import { + realpathSync as nativeRealpathSync, + openSync, + closeSync, + existsSync, + unlinkSync, +} from 'fs'; + +export const NAMESPACE_PREFIX = process.platform === 'win32' ? '\\\\?\\' : ''; + +/** + * Get a standardized reference to a path + * @param {string} path - the path to standardize + * @param {boolean} [usePosix=true] - produce a posix reference + * @param {boolean} [escapedBackslashes=true] - on Windows, double-backslash the reference + * @param {boolean} [returnUNC=false] - produce an extended reference + */ +export const standardize = ( + path: string, + usePosix: boolean = true, + escapedBackslashes: boolean = true, + returnUNC: boolean = false +) => { + // Force os-dependant separators + const normal = normalize(path); + + // Filter out in-browser executions as well as non-windows ones + if (process?.platform !== 'win32') return normal; + + if (usePosix) return normal.replace(/\\/g, '/'); + else if (escapedBackslashes) return normal.replace(/\\/g, '\\\\'); + else if (returnUNC) return '\\\\?\\' + normal; + return normal; +}; + +/** + * Windows-only function that uses PowerShell to calculate the full path + * @param {string} path + * @private + */ +const getFullPathSync = (path: string) => { + if (process.platform !== 'win32') return path; + + try { + const fullName = execSync(`powershell "(Get-Item -LiteralPath '${path}').FullName"`, { + encoding: 'utf8', + })?.trim?.(); + + // Make sure we got something back + if (fullName?.length > 2) return fullName; + } catch (ex) { + // Do nothing + } + + return path; +}; + +/** + * Windows-only function that uses PowerShell and Com Object to calculate the 8.3 path + * @param {string} path + * @private + */ +const getShortPathSync = (path: string) => { + if (process.platform !== 'win32') return path; + + try { + const shortPath = execSync( + `powershell "$FSO = New-Object -ComObject Scripting.FileSystemObject; $O = (Get-Item -LiteralPath '${path}'); if ($O.PSIsContainer) { $FSO.GetFolder($O.FullName).ShortPath } else { $FSO.GetFile($O.FullName).ShortPath }"`, + { + encoding: 'utf8', + } + )?.trim?.(); + + // Make sure we got something back + if (shortPath?.length > 2) return shortPath; + } catch (ex) { + // Do nothing + } + + return path; +}; + +/** + * Checks if Windows 8.3 short names are supported on the volume of the given path + * @param {string} [path='.'] - the path to examine + */ +export const shortNamesSupportedSync = (path: string = '.') => { + if (process.platform !== 'win32') return false; + + const testFileName = '.___osd-cross-platform-test.file'; + const file = resolve(path, testFileName); + + // Create a test file if it doesn't exist + if (!existsSync(file)) closeSync(openSync(file, 'w')); + + // If the returned value's basename is not the same as the requested file name, it must be a short name + const foundShortName = basename(getShortPathSync(file)) !== testFileName; + + // Cleanup + unlinkSync(file); + + return foundShortName; +}; + +/** + * @borrows shortNamesSupportedSync + */ +export const shortNameSupportedSync = shortNamesSupportedSync; + +/** + * Get the full pathname + * @param {string} path - the path to resolve + */ +export const resolveToFullPathSync = (path: string) => getFullPathSync(resolve(path)); + +/** + * @borrows resolveToFullPathSync + */ +export const resolveToFullNameSync = resolveToFullPathSync; + +/** + * Get the short pathname + * @param {string} path - the path to resolve + */ +export const resolveToShortPathSync = (path: string) => getShortPathSync(resolve(path)); + +/** + * @borrows resolveToShortPathSync + */ +export const resolveToShortNameSync = resolveToShortPathSync; + +/** + * Get the canonical pathname + * @param {string} path - the path to resolve + */ +export const realPathSync = (path: string) => getFullPathSync(nativeRealpathSync(path, 'utf8')); + +/** + * @borrows realPathSync + */ +export const realpathSync = realPathSync; + +/** + * Get the canonical pathname + * @param {string} path - the path to resolve + */ +export const realShortPathSync = (path: string) => + getShortPathSync(nativeRealpathSync(path, 'utf8')); + +/** + * @borrows realShortPathSync + */ +export const realshortpathSync = realShortPathSync; diff --git a/packages/osd-cross-platform/src/process.ts b/packages/osd-cross-platform/src/process.ts new file mode 100644 index 000000000000..ade19cca5615 --- /dev/null +++ b/packages/osd-cross-platform/src/process.ts @@ -0,0 +1,20 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { resolveToFullPathSync, standardize } from './path'; + +/** + * The full pathname of the working directory of the process + * @constant + * @type {string} + */ +export const PROCESS_WORKING_DIR: string = resolveToFullPathSync(process.cwd()); + +/** + * The full pathname of the working directory of the process, in POSIX format + * @constant + * @type {string} + */ +export const PROCESS_POSIX_WORKING_DIR: string = standardize(PROCESS_WORKING_DIR); diff --git a/packages/osd-cross-platform/tsconfig.json b/packages/osd-cross-platform/tsconfig.json new file mode 100644 index 000000000000..e9dd6313e6f7 --- /dev/null +++ b/packages/osd-cross-platform/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/osd-dev-utils/src/serializers/absolute_path_serializer.ts b/packages/osd-dev-utils/src/serializers/absolute_path_serializer.ts index dd7757c594da..a99411a81b65 100644 --- a/packages/osd-dev-utils/src/serializers/absolute_path_serializer.ts +++ b/packages/osd-dev-utils/src/serializers/absolute_path_serializer.ts @@ -28,14 +28,19 @@ * under the License. */ -import { REPO_ROOT } from '@osd/utils'; +import { REPO_ROOT, REPO_ROOT_8_3 } from '@osd/utils'; export function createAbsolutePathSerializer( - rootPath: string = REPO_ROOT, + rootPath: string | string[] = [REPO_ROOT, REPO_ROOT_8_3], replacement = '<absolute path>' ) { + const rootPaths = Array.isArray(rootPath) ? rootPath : [rootPath]; return { - test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), - serialize: (value: string) => value.replace(rootPath, replacement).replace(/\\/g, '/'), + test: (value: any) => + typeof value === 'string' && rootPaths.some((path) => value.startsWith(path)), + serialize: (value: string) => + rootPaths + .reduce((result, oath) => result.replace(oath, replacement), value) + .replace(/\\/g, '/'), }; } diff --git a/packages/osd-optimizer/package.json b/packages/osd-optimizer/package.json index 2edb72e9d1e0..e3ffe9a4930a 100644 --- a/packages/osd-optimizer/package.json +++ b/packages/osd-optimizer/package.json @@ -13,6 +13,7 @@ "@babel/cli": "^7.16.0", "@babel/core": "^7.16.5", "@osd/babel-preset": "1.0.0", + "@osd/cross-platform": "1.0.0", "@osd/dev-utils": "1.0.0", "@osd/std": "1.0.0", "@osd/ui-shared-deps": "1.0.0", diff --git a/packages/osd-optimizer/src/node/cache.ts b/packages/osd-optimizer/src/node/cache.ts index 225a0de79ee9..8200b155c80b 100644 --- a/packages/osd-optimizer/src/node/cache.ts +++ b/packages/osd-optimizer/src/node/cache.ts @@ -48,17 +48,24 @@ export class Cache { private readonly atimes: LmdbStore.Database<string, string>; private readonly mtimes: LmdbStore.Database<string, string>; private readonly sourceMaps: LmdbStore.Database<string, string>; - private readonly pathRoot: string; + private readonly pathRoots: string[]; private readonly prefix: string; private readonly log?: Writable; private readonly timer: NodeJS.Timer; - constructor(config: { pathRoot: string; dir: string; prefix: string; log?: Writable }) { - if (!Path.isAbsolute(config.pathRoot)) { + constructor(config: { + pathRoot: string | string[]; + dir: string; + prefix: string; + log?: Writable; + }) { + const pathRoots = Array.isArray(config.pathRoot) ? config.pathRoot : [config.pathRoot]; + + if (!pathRoots.every((pathRoot) => Path.isAbsolute(pathRoot))) { throw new Error('cache requires an absolute path to resolve paths relative to'); } - this.pathRoot = config.pathRoot; + this.pathRoots = pathRoots; this.prefix = config.prefix; this.log = config.log; @@ -139,10 +146,15 @@ export class Cache { } private getKey(path: string) { + const resolvedPath = Path.resolve(path); + // Try to find the root that is the parent to path + const pathRoot = + this.pathRoots.find((root) => resolvedPath.startsWith(root)) || this.pathRoots[0]; + const normalizedPath = Path.sep !== '/' - ? Path.relative(this.pathRoot, path).split(Path.sep).join('/') - : Path.relative(this.pathRoot, path); + ? Path.relative(pathRoot, resolvedPath).split(Path.sep).join('/') + : Path.relative(pathRoot, resolvedPath); return `${this.prefix}${normalizedPath}`; } diff --git a/packages/osd-optimizer/src/node/node_auto_tranpilation.ts b/packages/osd-optimizer/src/node/node_auto_tranpilation.ts index 123f5afb3e77..87fcde1ce9d3 100644 --- a/packages/osd-optimizer/src/node/node_auto_tranpilation.ts +++ b/packages/osd-optimizer/src/node/node_auto_tranpilation.ts @@ -50,7 +50,7 @@ import Crypto from 'crypto'; import * as babel from '@babel/core'; import { addHook } from 'pirates'; -import { REPO_ROOT, UPSTREAM_BRANCH } from '@osd/dev-utils'; +import { REPO_ROOT, REPO_ROOT_8_3, UPSTREAM_BRANCH } from '@osd/dev-utils'; import sourceMapSupport from 'source-map-support'; import { Cache } from './cache'; @@ -142,7 +142,7 @@ export function registerNodeAutoTranspilation() { installed = true; const cache = new Cache({ - pathRoot: REPO_ROOT, + pathRoot: [REPO_ROOT, REPO_ROOT_8_3], dir: Path.resolve(REPO_ROOT, 'data/node_auto_transpilation_cache_v1', UPSTREAM_BRANCH), prefix: determineCachePrefix(), log: process.env.DEBUG_NODE_TRANSPILER_CACHE diff --git a/packages/osd-optimizer/src/optimizer/cache_keys.test.ts b/packages/osd-optimizer/src/optimizer/cache_keys.test.ts index c97452132550..b01228a09574 100644 --- a/packages/osd-optimizer/src/optimizer/cache_keys.test.ts +++ b/packages/osd-optimizer/src/optimizer/cache_keys.test.ts @@ -28,8 +28,7 @@ * under the License. */ -import Path from 'path'; - +import fs from 'fs/promises'; import { diff } from 'jest-diff'; import { REPO_ROOT } from '@osd/utils'; import { createAbsolutePathSerializer } from '@osd/dev-utils'; @@ -52,14 +51,6 @@ jest.mock('./get_mtimes.ts', () => ({ jest.mock('execa'); -jest.mock('fs', () => { - const realFs = jest.requireActual('fs'); - return { - ...realFs, - readFile: jest.fn(realFs.readFile), - }; -}); - expect.addSnapshotSerializer(createAbsolutePathSerializer()); jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { @@ -83,17 +74,9 @@ jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], describe('getOptimizerCacheKey()', () => { it('uses latest commit, bootstrap cache, and changed files to create unique value', async () => { - jest - .requireMock('fs') - .readFile.mockImplementation( - (path: string, enc: string, cb: (err: null, file: string) => void) => { - expect(path).toBe( - Path.resolve(REPO_ROOT, 'packages/osd-optimizer/target/.bootstrap-cache') - ); - expect(enc).toBe('utf8'); - cb(null, '<bootstrap cache>'); - } - ); + const mockFSReadFileAsync = jest + .spyOn(fs, 'readFile') + .mockReturnValue(Promise.resolve('<bootstrap cache>')); const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, @@ -122,6 +105,8 @@ describe('getOptimizerCacheKey()', () => { }, } `); + + mockFSReadFileAsync.mockRestore(); }); }); diff --git a/packages/osd-optimizer/src/optimizer/cache_keys.ts b/packages/osd-optimizer/src/optimizer/cache_keys.ts index e31009f872c5..16efdea4888b 100644 --- a/packages/osd-optimizer/src/optimizer/cache_keys.ts +++ b/packages/osd-optimizer/src/optimizer/cache_keys.ts @@ -28,13 +28,12 @@ * under the License. */ -import Path from 'path'; -import Fs from 'fs'; -import { promisify } from 'util'; +import { dirname, resolve } from 'path'; +import { readFile } from 'fs/promises'; import Chalk from 'chalk'; import execa from 'execa'; -import { REPO_ROOT } from '@osd/utils'; +import { relativeToRepoRoot, REPO_ROOT } from '@osd/cross-platform'; import stripAnsi from 'strip-ansi'; import { diff } from 'jest-diff'; @@ -45,8 +44,8 @@ import { getMtimes } from './get_mtimes'; import { getChanges } from './get_changes'; import { OptimizerConfig } from './optimizer_config'; -const OPTIMIZER_DIR = Path.dirname(require.resolve('../../package.json')); -const RELATIVE_DIR = Path.relative(REPO_ROOT, OPTIMIZER_DIR); +const OPTIMIZER_DIR = dirname(require.resolve('../../package.json')); +const RELATIVE_DIR = relativeToRepoRoot(OPTIMIZER_DIR)!; export function diffCacheKey(expected?: unknown, actual?: unknown) { const expectedJson = jsonStable(expected, { @@ -156,10 +155,7 @@ async function getLastCommit() { async function getBootstrapCacheKey() { try { - return await promisify(Fs.readFile)( - Path.resolve(OPTIMIZER_DIR, 'target/.bootstrap-cache'), - 'utf8' - ); + return await readFile(resolve(OPTIMIZER_DIR, 'target/.bootstrap-cache'), { encoding: 'utf8' }); } catch (error) { if (error?.code !== 'ENOENT') { throw error; diff --git a/packages/osd-optimizer/src/optimizer/get_changes.test.ts b/packages/osd-optimizer/src/optimizer/get_changes.test.ts index 44e1637d5437..6ccf686e43af 100644 --- a/packages/osd-optimizer/src/optimizer/get_changes.test.ts +++ b/packages/osd-optimizer/src/optimizer/get_changes.test.ts @@ -33,7 +33,7 @@ import path from 'path'; jest.mock('execa'); import { getChanges } from './get_changes'; -import { standardize } from '@osd/dev-utils'; +import { standardize } from '@osd/cross-platform'; const execa: jest.Mock = jest.requireMock('execa'); diff --git a/packages/osd-plugin-generator/package.json b/packages/osd-plugin-generator/package.json index 73aaeba4cb13..66028c53875f 100644 --- a/packages/osd-plugin-generator/package.json +++ b/packages/osd-plugin-generator/package.json @@ -9,6 +9,7 @@ "osd:watch": "node scripts/build --watch" }, "dependencies": { + "@osd/cross-platform": "1.0.0", "@osd/dev-utils": "1.0.0", "ejs": "^3.1.7", "execa": "^4.0.2", diff --git a/packages/osd-plugin-generator/src/integration_tests/generate_plugin.test.ts b/packages/osd-plugin-generator/src/integration_tests/generate_plugin.test.ts index 45ec5a6986a1..38ad0496c176 100644 --- a/packages/osd-plugin-generator/src/integration_tests/generate_plugin.test.ts +++ b/packages/osd-plugin-generator/src/integration_tests/generate_plugin.test.ts @@ -32,7 +32,8 @@ import Path from 'path'; import del from 'del'; import execa from 'execa'; -import { REPO_ROOT, standardize, createAbsolutePathSerializer } from '@osd/dev-utils'; +import { standardize, REPO_ROOT } from '@osd/cross-platform'; +import { createAbsolutePathSerializer } from '@osd/dev-utils'; import globby from 'globby'; // Has to be a posix reference because it is used to generate glob patterns diff --git a/packages/osd-plugin-helpers/package.json b/packages/osd-plugin-helpers/package.json index 3738aae36b1a..55e5e803fe47 100644 --- a/packages/osd-plugin-helpers/package.json +++ b/packages/osd-plugin-helpers/package.json @@ -16,6 +16,7 @@ "osd:watch": "tsc --watch" }, "dependencies": { + "@osd/cross-platform": "1.0.0", "@osd/dev-utils": "1.0.0", "@osd/optimizer": "1.0.0", "del": "^5.1.0", diff --git a/packages/osd-plugin-helpers/src/cli.ts b/packages/osd-plugin-helpers/src/cli.ts index 82bd9f5adbe4..f108f9a2f6ca 100644 --- a/packages/osd-plugin-helpers/src/cli.ts +++ b/packages/osd-plugin-helpers/src/cli.ts @@ -30,6 +30,7 @@ import Path from 'path'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; import { RunWithCommands, createFlagError, createFailError } from '@osd/dev-utils'; import { findOpenSearchDashboardsJson } from './find_opensearch_dashboards_json'; @@ -79,10 +80,10 @@ export function runCli() { throw createFlagError('expected a single --skip-archive flag'); } - const pluginDir = await findOpenSearchDashboardsJson(process.cwd()); + const pluginDir = await findOpenSearchDashboardsJson(PROCESS_WORKING_DIR); if (!pluginDir) { throw createFailError( - `Unable to find OpenSearch Dashboards Platform plugin in [${process.cwd()}] or any of its parent directories. Has it been migrated properly? Does it have a opensearch_dashboards.json file?` + `Unable to find OpenSearch Dashboards Platform plugin in [${PROCESS_WORKING_DIR}] or any of its parent directories. Has it been migrated properly? Does it have a opensearch_dashboards.json file?` ); } @@ -148,30 +149,30 @@ export function runCli() { allowUnexpected: true, }, async run({ log, flags }) { - const pluginDir = await findOpenSearchDashboardsJson(process.cwd()); + const pluginDir = await findOpenSearchDashboardsJson(PROCESS_WORKING_DIR); if (!pluginDir) { throw createFailError( - `Unable to find OpenSearch Dashboards Platform plugin in [${process.cwd()}] or any of its parent directories. Has it been migrated properly? Does it have a opensearch_dashboards.json file?` + `Unable to find OpenSearch Dashboards Platform plugin in [${PROCESS_WORKING_DIR}] or any of its parent directories. Has it been migrated properly? Does it have a opensearch_dashboards.json file?` ); } let dashboardsPackage; try { - dashboardsPackage = await import(Path.join(process.cwd(), '../../package.json')); + dashboardsPackage = await import(Path.join(PROCESS_WORKING_DIR, '../../package.json')); } catch (ex) { throw createFailError(`Unable to parse the OpenSearch Dashboards' package.json file`); } let pluginPackage; try { - pluginPackage = await import(Path.join(process.cwd(), 'package.json')); + pluginPackage = await import(Path.join(PROCESS_WORKING_DIR, 'package.json')); } catch (ex) { throw createFailError(`Unable to parse the plugin's package.json file`); } let manifestFile; try { - manifestFile = await import(Path.join(process.cwd(), 'opensearch_dashboards.json')); + manifestFile = await import(Path.join(PROCESS_WORKING_DIR, 'opensearch_dashboards.json')); } catch (ex) { throw createFailError(`Unable to parse the plugin's opensearch_dashboards.json file`); } @@ -240,7 +241,7 @@ export function runCli() { const context: VersionContext = { log, - sourceDir: process.cwd(), + sourceDir: PROCESS_WORKING_DIR, pluginVersion: updatedPluginVersion, compatibilityVersion: updatedCompatibilityVersion, }; diff --git a/packages/osd-plugin-helpers/src/integration_tests/build.test.ts b/packages/osd-plugin-helpers/src/integration_tests/build.test.ts index 35195f9bc163..3d32f8c064ca 100644 --- a/packages/osd-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/osd-plugin-helpers/src/integration_tests/build.test.ts @@ -32,12 +32,8 @@ import Path from 'path'; import Fs from 'fs'; import execa from 'execa'; -import { - REPO_ROOT, - standardize, - createStripAnsiSerializer, - createReplaceSerializer, -} from '@osd/dev-utils'; +import { standardize, REPO_ROOT } from '@osd/cross-platform'; +import { createStripAnsiSerializer, createReplaceSerializer } from '@osd/dev-utils'; import extract from 'extract-zip'; import del from 'del'; import globby from 'globby'; diff --git a/packages/osd-plugin-helpers/src/tasks/create_archive.ts b/packages/osd-plugin-helpers/src/tasks/create_archive.ts index 566fc83ff212..9d0ee1be278d 100644 --- a/packages/osd-plugin-helpers/src/tasks/create_archive.ts +++ b/packages/osd-plugin-helpers/src/tasks/create_archive.ts @@ -62,6 +62,7 @@ export async function createArchive({ opensearchDashboardsVersion, plugin, log } vfs.dest(buildDir) ); + log.info(`cleaning up compression temporary artifacts`); // delete the files that were zipped - await del(Path.resolve(buildDir, 'opensearch-dashboards')); + await del(Path.resolve(buildDir, 'opensearch-dashboards'), { cwd: buildDir }); } diff --git a/packages/osd-pm/package.json b/packages/osd-pm/package.json index 3af720ce1691..cda579e14fef 100644 --- a/packages/osd-pm/package.json +++ b/packages/osd-pm/package.json @@ -69,6 +69,7 @@ "write-pkg": "^4.0.0" }, "dependencies": { + "@osd/cross-platform": "1.0.0", "@osd/utils": "1.0.0", "tslib": "^2.0.0" } diff --git a/packages/osd-pm/src/utils/projects.test.ts b/packages/osd-pm/src/utils/projects.test.ts index 545b435a7e09..a4c945647f6e 100644 --- a/packages/osd-pm/src/utils/projects.test.ts +++ b/packages/osd-pm/src/utils/projects.test.ts @@ -43,12 +43,16 @@ import { ProjectMap, topologicallyBatchProjects, } from './projects'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const rootPath = resolve(__dirname, '__fixtures__/opensearch-dashboards'); const rootPlugins = join(rootPath, 'plugins'); describe('#getProjects', () => { beforeAll(async () => { + // Make sure we start clean + await del(rootPlugins, { cwd: PROCESS_WORKING_DIR }); + await promisify(mkdir)(rootPlugins); await promisify(symlink)( @@ -58,7 +62,7 @@ describe('#getProjects', () => { ); }); - afterAll(async () => await del(rootPlugins)); + afterAll(async () => await del(rootPlugins, { cwd: PROCESS_WORKING_DIR })); test('find all packages in the packages directory', async () => { const projects = await getProjects(rootPath, ['packages/*']); diff --git a/packages/osd-pm/src/utils/projects_tree.ts b/packages/osd-pm/src/utils/projects_tree.ts index 41f9e4331098..3c14be94ca0f 100644 --- a/packages/osd-pm/src/utils/projects_tree.ts +++ b/packages/osd-pm/src/utils/projects_tree.ts @@ -31,7 +31,7 @@ import chalk from 'chalk'; import path from 'path'; -import { standardize } from '@osd/utils'; +import { standardize } from '@osd/cross-platform'; import { Project } from './project'; const projectKey = Symbol('__project'); diff --git a/packages/osd-utils/package.json b/packages/osd-utils/package.json index 802b03a0adb0..1d632cffbf69 100644 --- a/packages/osd-utils/package.json +++ b/packages/osd-utils/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@osd/config-schema": "1.0.0", + "@osd/cross-platform": "1.0.0", "load-json-file": "^6.2.0" }, "devDependencies": { diff --git a/packages/osd-utils/src/index.ts b/packages/osd-utils/src/index.ts index 7fd473a67b98..f5bf23b4164d 100644 --- a/packages/osd-utils/src/index.ts +++ b/packages/osd-utils/src/index.ts @@ -30,4 +30,4 @@ export * from './package_json'; export * from './path'; -export * from './repo_root'; +export { REPO_ROOT, REPO_ROOT_8_3, UPSTREAM_BRANCH } from '@osd/cross-platform'; diff --git a/packages/osd-utils/src/package_json/index.ts b/packages/osd-utils/src/package_json/index.ts index 644554387ce6..bcd22bb28bab 100644 --- a/packages/osd-utils/src/package_json/index.ts +++ b/packages/osd-utils/src/package_json/index.ts @@ -29,7 +29,7 @@ */ import { dirname, resolve } from 'path'; -import { REPO_ROOT } from '../repo_root'; +import { REPO_ROOT } from '@osd/cross-platform'; export const opensearchDashboardsPackageJSON = { __filename: resolve(REPO_ROOT, 'package.json'), diff --git a/packages/osd-utils/src/path/index.ts b/packages/osd-utils/src/path/index.ts index 263d2d39ac36..ac4c40b0b606 100644 --- a/packages/osd-utils/src/path/index.ts +++ b/packages/osd-utils/src/path/index.ts @@ -28,10 +28,10 @@ * under the License. */ -import { join, normalize } from 'path'; +import { join } from 'path'; import { accessSync, constants } from 'fs'; import { TypeOf, schema } from '@osd/config-schema'; -import { REPO_ROOT } from '../repo_root'; +import { REPO_ROOT } from '@osd/cross-platform'; const isString = (v: any): v is string => typeof v === 'string'; @@ -94,27 +94,3 @@ export const config = { data: schema.string({ defaultValue: () => getDataPath() }), }), }; - -/** - * Get a standardized reference to a path - * @param {string} path - the path to standardize - * @param {boolean} [usePosix=true] - produce a posix reference - * @param {boolean} [escapedBackslashes=true] - on Windows, double-backslash the reference - * @internal - */ -export const standardize = ( - path: string, - usePosix: boolean = true, - escapedBackslashes: boolean = true -) => { - /* Force os-dependant separators - * path.posix.normalize doesn't convert backslashes to slashes on Windows so we manually force it afterwards - */ - const normal = normalize(path); - - // Filter out in-browser executions as well as non-windows ones - if (process?.platform !== 'win32') return normal; - - if (usePosix) return normal.replace(/\\/g, '/'); - return escapedBackslashes ? normal.replace(/\\/g, '\\\\') : normal; -}; diff --git a/packages/osd-utils/src/repo_root.ts b/packages/osd-utils/src/repo_root.ts index e46f62746b01..aea7b4e80493 100644 --- a/packages/osd-utils/src/repo_root.ts +++ b/packages/osd-utils/src/repo_root.ts @@ -28,21 +28,16 @@ * under the License. */ -import Path from 'path'; -import Fs from 'fs'; +import { resolve, parse, dirname, isAbsolute, relative } from 'path'; import loadJsonFile from 'load-json-file'; +import { resolveToFullPathSync, resolveToShortPathSync, realPathSync } from './path'; const readOpenSearchDashboardsPkgJson = (dir: string) => { try { - const path = Path.resolve(dir, 'package.json'); - const json = loadJsonFile.sync(path); - if ( - json && - typeof json === 'object' && - 'name' in json && - json.name === 'opensearch-dashboards' - ) { + const path = resolve(dir, 'package.json'); + const json = loadJsonFile.sync(path) as { [key: string]: any }; + if (json?.name === 'opensearch-dashboards') { return json; } } catch (error) { @@ -58,8 +53,8 @@ const findOpenSearchDashboardsPackageJson = () => { // search for the opensearch-dashboards directory, since this file is moved around it might // not be where we think but should always be a relatively close parent // of this directory - const startDir = Fs.realpathSync(__dirname); - const { root: rootDir } = Path.parse(startDir); + const startDir = realPathSync(__dirname); + const { root: rootDir } = parse(startDir); let cursor = startDir; while (true) { const opensearchDashboardsPkgJson = readOpenSearchDashboardsPkgJson(cursor); @@ -73,7 +68,7 @@ const findOpenSearchDashboardsPackageJson = () => { }; } - const parent = Path.dirname(cursor); + const parent = dirname(cursor); if (parent === rootDir) { throw new Error(`unable to find opensearch-dashboards directory from ${startDir}`); } @@ -86,5 +81,21 @@ const { opensearchDashboardsPkgJson, } = findOpenSearchDashboardsPackageJson(); -export const REPO_ROOT = opensearchDashboardsDir; +export const REPO_ROOT = resolveToFullPathSync(opensearchDashboardsDir); +export const REPO_ROOT_8_3 = resolveToShortPathSync(opensearchDashboardsDir); export const UPSTREAM_BRANCH = opensearchDashboardsPkgJson.branch; + +export const getRepoRoot = (path: string) => { + // We can only find the appropriate root if an absolute path was given + if (path && isAbsolute(path)) { + if (path.startsWith(REPO_ROOT)) return REPO_ROOT; + if (path.startsWith(REPO_ROOT_8_3)) return REPO_ROOT_8_3; + } + + return null; +}; + +export const relativeToRepoRoot = (path: string) => { + const root = getRepoRoot(path); + return root ? relative(root, path) : null; +}; diff --git a/src/cli_plugin/install/download.test.js b/src/cli_plugin/install/download.test.js index a847a92733a0..126af128b11d 100644 --- a/src/cli_plugin/install/download.test.js +++ b/src/cli_plugin/install/download.test.js @@ -28,7 +28,7 @@ * under the License. */ -import Fs from 'fs'; +import { mkdir } from 'fs/promises'; import { join } from 'path'; import http from 'http'; @@ -40,6 +40,7 @@ import del from 'del'; import { Logger } from '../lib/logger'; import { UnsupportedProtocolError } from '../lib/errors'; import { download, _downloadSingle, _getFilePath, _checkFilePathDeprecation } from './download'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; describe('opensearchDashboards cli', function () { describe('plugin downloader', function () { @@ -70,17 +71,17 @@ describe('opensearchDashboards cli', function () { throw new Error('expected the promise to reject'); } - beforeEach(function () { + beforeEach(async () => { sinon.stub(logger, 'log'); sinon.stub(logger, 'error'); - del.sync(testWorkingPath); - Fs.mkdirSync(testWorkingPath, { recursive: true }); + await del(testWorkingPath, { cwd: PROCESS_WORKING_DIR }); + await mkdir(testWorkingPath, { recursive: true }); }); - afterEach(function () { + afterEach(async () => { logger.log.restore(); logger.error.restore(); - del.sync(testWorkingPath); + del(testWorkingPath, { cwd: PROCESS_WORKING_DIR }); }); describe('_downloadSingle', function () { diff --git a/src/cli_plugin/install/opensearch_dashboards.test.js b/src/cli_plugin/install/opensearch_dashboards.test.js index 2a219c9ba6cf..74d2a675fe96 100644 --- a/src/cli_plugin/install/opensearch_dashboards.test.js +++ b/src/cli_plugin/install/opensearch_dashboards.test.js @@ -30,12 +30,14 @@ import { join } from 'path'; import fs from 'fs'; +import { mkdir } from 'fs/promises'; import sinon from 'sinon'; import del from 'del'; import { existingInstall, assertVersion } from './opensearch_dashboards'; import { Logger } from '../lib/logger'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; jest.spyOn(fs, 'statSync'); @@ -62,17 +64,17 @@ describe('opensearchDashboards cli', function () { const logger = new Logger(settings); describe('assertVersion', function () { - beforeEach(function () { - del.sync(testWorkingPath); - fs.mkdirSync(testWorkingPath, { recursive: true }); + beforeEach(async () => { + await del(testWorkingPath, { cwd: PROCESS_WORKING_DIR }); + await mkdir(testWorkingPath, { recursive: true }); sinon.stub(logger, 'log'); sinon.stub(logger, 'error'); }); - afterEach(function () { + afterEach(async () => { logger.log.restore(); logger.error.restore(); - del.sync(testWorkingPath); + await del(testWorkingPath, { cwd: PROCESS_WORKING_DIR }); }); it('should succeed with exact match', function () { diff --git a/src/cli_plugin/install/pack.test.js b/src/cli_plugin/install/pack.test.js index 783593c6d9fa..fa8f51fa2733 100644 --- a/src/cli_plugin/install/pack.test.js +++ b/src/cli_plugin/install/pack.test.js @@ -28,8 +28,8 @@ * under the License. */ -import Fs from 'fs'; import { join } from 'path'; +import { mkdir } from 'fs/promises'; import sinon from 'sinon'; import glob from 'glob-all'; @@ -38,6 +38,7 @@ import del from 'del'; import { Logger } from '../lib/logger'; import { extract, getPackData } from './pack'; import { _downloadSingle } from './download'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; describe('opensearchDashboards cli', function () { describe('pack', function () { @@ -49,7 +50,7 @@ describe('opensearchDashboards cli', function () { let logger; let settings; - beforeEach(function () { + beforeEach(async () => { //These tests are dependent on the file system, and I had some inconsistent //behavior with del.sync show up. Until these tests are re-written to not //depend on the file system, I make sure that each test uses a different @@ -69,14 +70,14 @@ describe('opensearchDashboards cli', function () { logger = new Logger(settings); sinon.stub(logger, 'log'); sinon.stub(logger, 'error'); - Fs.mkdirSync(testWorkingPath, { recursive: true }); + await mkdir(testWorkingPath, { recursive: true }); }); afterEach(async () => { logger.log.restore(); logger.error.restore(); - await del(workingPathRoot); + await del(workingPathRoot, { cwd: PROCESS_WORKING_DIR }); }); function copyReplyFile(filename) { diff --git a/src/cli_plugin/list/list.test.js b/src/cli_plugin/list/list.test.js index 7ae9663f73fb..724b7c4ec67a 100644 --- a/src/cli_plugin/list/list.test.js +++ b/src/cli_plugin/list/list.test.js @@ -30,10 +30,12 @@ import { join } from 'path'; import { writeFileSync, mkdirSync } from 'fs'; +import { mkdir } from 'fs/promises'; import del from 'del'; import { list } from './list'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; function createPlugin(name, version, pluginBaseDir) { const pluginDir = join(pluginBaseDir, name); @@ -61,14 +63,14 @@ describe('opensearchDashboards cli', function () { describe('plugin lister', function () { const pluginDir = join(__dirname, '.test.data.list'); - beforeEach(function () { + beforeEach(async () => { logger.messages.length = 0; - del.sync(pluginDir); - mkdirSync(pluginDir, { recursive: true }); + await del(pluginDir, { cwd: PROCESS_WORKING_DIR }); + await mkdir(pluginDir, { recursive: true }); }); - afterEach(function () { - del.sync(pluginDir); + afterEach(async () => { + await del(pluginDir, { cwd: PROCESS_WORKING_DIR }); }); it('list all of the folders in the plugin folder, ignoring dot prefixed plugins and regular files', function () { diff --git a/src/cli_plugin/remove/remove.test.js b/src/cli_plugin/remove/remove.test.js index 8c963df2de54..e4af3d1dd57a 100644 --- a/src/cli_plugin/remove/remove.test.js +++ b/src/cli_plugin/remove/remove.test.js @@ -30,6 +30,7 @@ import { join } from 'path'; import { writeFileSync, mkdirSync } from 'fs'; +import { mkdir } from 'fs/promises'; import sinon from 'sinon'; import glob from 'glob-all'; @@ -37,6 +38,7 @@ import del from 'del'; import { Logger } from '../lib/logger'; import { remove } from './remove'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; describe('opensearchDashboards cli', function () { describe('plugin remover', function () { @@ -46,20 +48,20 @@ describe('opensearchDashboards cli', function () { const settings = { pluginDir }; - beforeEach(function () { + beforeEach(async () => { processExitStub = sinon.stub(process, 'exit'); logger = new Logger(settings); sinon.stub(logger, 'log'); sinon.stub(logger, 'error'); - del.sync(pluginDir); - mkdirSync(pluginDir, { recursive: true }); + await del(pluginDir, { cwd: PROCESS_WORKING_DIR }); + await mkdir(pluginDir, { recursive: true }); }); - afterEach(function () { + afterEach(async () => { processExitStub.restore(); logger.log.restore(); logger.error.restore(); - del.sync(pluginDir); + await del(pluginDir, { cwd: PROCESS_WORKING_DIR }); }); it('throw an error if the plugin is not installed.', function () { diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 92b2cb71ef98..550acaf3f4f3 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -42,9 +42,7 @@ import { PluginsConfig, PluginsConfigType, config } from '../plugins_config'; import type { InstanceInfo } from '../plugin_context'; import { discover } from './plugins_discovery'; import { CoreContext } from '../../core_context'; - -const OPENSEARCH_DASHBOARDS_ROOT = process.cwd(); -const EXTENDED_PATH_PREFIX = process.platform === 'win32' ? '\\\\?\\' : ''; +import { PROCESS_WORKING_DIR, standardize } from '@osd/cross-platform'; const Plugins = { invalid: () => ({ @@ -87,13 +85,7 @@ const packageMock = { }; const manifestPath = (...pluginPath: string[]) => - resolve( - OPENSEARCH_DASHBOARDS_ROOT, - 'src', - 'plugins', - ...pluginPath, - 'opensearch_dashboards.json' - ); + resolve(PROCESS_WORKING_DIR, 'src', 'plugins', ...pluginPath, 'opensearch_dashboards.json'); describe('plugins discovery system', () => { let logger: ReturnType<typeof loggingSystemMock.create>; @@ -157,8 +149,8 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/plugins/plugin_b`]: Plugins.valid('pluginB'), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), + [`${PROCESS_WORKING_DIR}/plugins/plugin_b`]: Plugins.valid('pluginB'), }, { createCwd: false } ); @@ -179,10 +171,10 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: Plugins.invalid(), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_b`]: Plugins.incomplete(), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_c`]: Plugins.incompatible(), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_ad`]: Plugins.missingManifest(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: Plugins.invalid(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_b`]: Plugins.incomplete(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_c`]: Plugins.incompatible(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_ad`]: Plugins.missingManifest(), }, { createCwd: false } ); @@ -221,7 +213,7 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins`]: mockFs.directory({ + [`${PROCESS_WORKING_DIR}/src/plugins`]: mockFs.directory({ mode: 0, // 0000 items: { plugin_a: Plugins.valid('pluginA'), @@ -241,10 +233,15 @@ describe('plugins discovery system', () => { ) .toPromise(); - const srcPluginsPath = resolve(OPENSEARCH_DASHBOARDS_ROOT, 'src', 'plugins'); + const srcPluginsPath = resolve(PROCESS_WORKING_DIR, 'src', 'plugins'); expect(errors).toEqual( expect.arrayContaining([ - `Error: EACCES, permission denied '${EXTENDED_PATH_PREFIX}${srcPluginsPath}' (invalid-search-path, ${srcPluginsPath})`, + `Error: EACCES, permission denied '${standardize( + srcPluginsPath, + false, + false, + true + )}' (invalid-search-path, ${srcPluginsPath})`, ]) ); }); @@ -258,7 +255,7 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: { + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: { ...Plugins.inaccessibleManifest(), nested_plugin: Plugins.valid('nestedPlugin'), }, @@ -279,7 +276,12 @@ describe('plugins discovery system', () => { const errorPath = manifestPath('plugin_a'); expect(errors).toEqual( expect.arrayContaining([ - `Error: EACCES, permission denied '${EXTENDED_PATH_PREFIX}${errorPath}' (missing-manifest, ${errorPath})`, + `Error: EACCES, permission denied '${standardize( + errorPath, + false, + false, + true + )}' (missing-manifest, ${errorPath})`, ]) ); }); @@ -293,10 +295,10 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/plugin_b`]: Plugins.valid('pluginB'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/plugin_c`]: Plugins.valid('pluginC'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/plugin_d`]: Plugins.incomplete(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/plugin_b`]: Plugins.valid('pluginB'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/plugin_c`]: Plugins.valid('pluginC'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/plugin_d`]: Plugins.incomplete(), }, { createCwd: false } ); @@ -330,7 +332,7 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: { + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: { ...Plugins.valid('pluginA'), nested_plugin: Plugins.valid('nestedPlugin'), }, @@ -349,18 +351,14 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/plugin`]: Plugins.valid('plugin1'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/plugin`]: Plugins.valid('plugin2'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/sub3/plugin`]: Plugins.valid( - 'plugin3' - ), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/sub3/sub4/plugin`]: Plugins.valid( - 'plugin4' - ), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/sub3/sub4/sub5/plugin`]: Plugins.valid( + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/plugin`]: Plugins.valid('plugin1'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/plugin`]: Plugins.valid('plugin2'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/sub3/plugin`]: Plugins.valid('plugin3'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/sub3/sub4/plugin`]: Plugins.valid('plugin4'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/sub3/sub4/sub5/plugin`]: Plugins.valid( 'plugin5' ), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/sub3/sub4/sub5/sub6/plugin`]: Plugins.valid( + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/sub3/sub4/sub5/sub6/plugin`]: Plugins.valid( 'plugin6' ), }, @@ -379,11 +377,11 @@ describe('plugins discovery system', () => { it('works with symlinks', async () => { const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext, instanceInfo); - const pluginFolder = resolve(OPENSEARCH_DASHBOARDS_ROOT, '..', 'ext-plugins'); + const pluginFolder = resolve(PROCESS_WORKING_DIR, '..', 'ext-plugins'); mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/plugins`]: mockFs.symlink({ + [`${PROCESS_WORKING_DIR}/plugins`]: mockFs.symlink({ path: '../ext-plugins', }), [pluginFolder]: { diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index cff5ae79b914..b7dd4e64c016 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -33,7 +33,8 @@ import { mockDiscover, mockPackage } from './plugins_service.test.mocks'; import { resolve, posix } from 'path'; import { BehaviorSubject, from } from 'rxjs'; import { schema } from '@osd/config-schema'; -import { createAbsolutePathSerializer, REPO_ROOT } from '@osd/dev-utils'; +import { getRepoRoot } from '@osd/cross-platform'; +import { createAbsolutePathSerializer } from '@osd/dev-utils'; import { ConfigPath, ConfigService, Env } from '../config'; import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; @@ -127,7 +128,7 @@ describe('PluginsService', () => { }; coreId = Symbol('core'); - env = Env.createDefault(REPO_ROOT, getEnvOptions()); + env = Env.createDefault(getRepoRoot(resolve('.'))!, getEnvOptions()); config$ = new BehaviorSubject<Record<string, any>>({ plugins: { initialize: true } }); const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ }); diff --git a/src/dev/build/lib/config.test.ts b/src/dev/build/lib/config.test.ts index db96a8c18dd0..a625aab9e256 100644 --- a/src/dev/build/lib/config.test.ts +++ b/src/dev/build/lib/config.test.ts @@ -30,7 +30,7 @@ import { resolve } from 'path'; -import { REPO_ROOT, standardize } from '@osd/utils'; +import { standardize, REPO_ROOT } from '@osd/cross-platform'; import { createAbsolutePathSerializer } from '@osd/dev-utils'; import pkg from '../../../../package.json'; diff --git a/src/dev/build/lib/integration_tests/fs.test.ts b/src/dev/build/lib/integration_tests/fs.test.ts index 0cf777cce662..1ad9afa54ef8 100644 --- a/src/dev/build/lib/integration_tests/fs.test.ts +++ b/src/dev/build/lib/integration_tests/fs.test.ts @@ -34,6 +34,7 @@ import { chmodSync, statSync } from 'fs'; import del from 'del'; import { mkdirp, write, read, getChildPaths, copyAll, getFileHash, untar, gunzip } from '../fs'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const TMP = resolve(__dirname, '../__tmp__'); const FIXTURES = resolve(__dirname, '../__fixtures__'); @@ -61,13 +62,13 @@ beforeAll(async () => { // clean and recreate TMP directory beforeEach(async () => { - await del(TMP); + await del(TMP, { cwd: PROCESS_WORKING_DIR }); await mkdirp(TMP); }); // cleanup TMP directory afterAll(async () => { - await del(TMP); + await del(TMP, { cwd: PROCESS_WORKING_DIR }); }); describe('mkdirp()', () => { diff --git a/src/dev/build/lib/integration_tests/scan_copy.test.ts b/src/dev/build/lib/integration_tests/scan_copy.test.ts index 9ed5aaa1d484..bf010ef8a13d 100644 --- a/src/dev/build/lib/integration_tests/scan_copy.test.ts +++ b/src/dev/build/lib/integration_tests/scan_copy.test.ts @@ -35,6 +35,7 @@ import del from 'del'; import { getChildPaths } from '../fs'; import { scanCopy } from '../scan_copy'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const IS_WINDOWS = process.platform === 'win32'; const FIXTURES = resolve(__dirname, '../__fixtures__'); @@ -50,7 +51,7 @@ beforeAll(async () => { // cleanup TMP directory afterEach(async () => { - await del(TMP); + await del(TMP, { cwd: PROCESS_WORKING_DIR }); }); it('rejects if source path is not absolute', async () => { diff --git a/src/dev/build/lib/scan_delete.test.ts b/src/dev/build/lib/scan_delete.test.ts index 2f8f5473ad04..900b5b52af33 100644 --- a/src/dev/build/lib/scan_delete.test.ts +++ b/src/dev/build/lib/scan_delete.test.ts @@ -36,12 +36,16 @@ import del from 'del'; // @ts-ignore import { mkdirp, write } from './fs'; import { scanDelete } from './scan_delete'; +import { PROCESS_WORKING_DIR, getRepoRoot } from '@osd/cross-platform'; -const TMP = resolve(__dirname, '__tests__/__tmp__'); +const rootPath = getRepoRoot(__dirname); +const currentDir = rootPath ? resolve('.', relative(rootPath, __dirname)) : __dirname; + +const TMP = resolve(currentDir, '__tests__/__tmp__'); // clean and recreate TMP directory beforeEach(async () => { - await del(TMP); + await del(TMP, { cwd: currentDir }); await mkdirp(resolve(TMP, 'foo/bar/baz')); await mkdirp(resolve(TMP, 'foo/bar/box')); await mkdirp(resolve(TMP, 'a/b/c/d/e')); @@ -50,13 +54,13 @@ beforeEach(async () => { // cleanup TMP directory afterAll(async () => { - await del(TMP); + await del(TMP, { cwd: currentDir }); }); it('requires absolute paths', async () => { await expect( scanDelete({ - directory: relative(process.cwd(), TMP), + directory: relative(PROCESS_WORKING_DIR, TMP), regularExpressions: [], }) ).rejects.toMatchInlineSnapshot( diff --git a/src/dev/file.ts b/src/dev/file.ts index 7d959387995c..48c62234a27d 100644 --- a/src/dev/file.ts +++ b/src/dev/file.ts @@ -29,6 +29,7 @@ */ import { dirname, extname, join, relative, resolve, sep, basename } from 'path'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; export class File { private path: string; @@ -37,7 +38,7 @@ export class File { constructor(path: string) { this.path = resolve(path); - this.relativePath = relative(process.cwd(), this.path); + this.relativePath = relative(PROCESS_WORKING_DIR, this.path); this.ext = extname(this.path); } diff --git a/src/dev/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts index 8bec5b7e740a..f11419868faa 100644 --- a/src/dev/i18n/integrate_locale_files.test.ts +++ b/src/dev/i18n/integrate_locale_files.test.ts @@ -30,11 +30,12 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.test.mocks'; -import path from 'path'; +import { resolve } from 'path'; import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files'; -import { normalizePath } from './utils'; +import { relativeToRepoRoot, standardize } from '@osd/cross-platform'; -const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); +const currentDir = relativeToRepoRoot(__dirname); +const localePath = resolve(currentDir, '__fixtures__', 'integrate_locale_files', 'fr.json'); const mockDefaultMessagesMap = new Map([ ['plugin-1.message-id-1', { message: 'Message text 1' }], @@ -180,9 +181,12 @@ Map { const [[path1, json1], [path2, json2]] = mockWriteFileAsync.mock.calls; const [[dirPath1], [dirPath2]] = mockMakeDirAsync.mock.calls; - expect([normalizePath(path1), json1]).toMatchSnapshot(); - expect([normalizePath(path2), json2]).toMatchSnapshot(); - expect([normalizePath(dirPath1), normalizePath(dirPath2)]).toMatchSnapshot(); + expect([standardize(relativeToRepoRoot(path1)), json1]).toMatchSnapshot(); + expect([standardize(relativeToRepoRoot(path2)), json2]).toMatchSnapshot(); + expect([ + standardize(relativeToRepoRoot(dirPath1)), + standardize(relativeToRepoRoot(dirPath2)), + ]).toMatchSnapshot(); }); }); }); diff --git a/src/dev/i18n/utils/utils.js b/src/dev/i18n/utils/utils.js index 370a49e466f5..79868ddd6314 100644 --- a/src/dev/i18n/utils/utils.js +++ b/src/dev/i18n/utils/utils.js @@ -48,6 +48,7 @@ import chalk from 'chalk'; import parser from 'intl-messageformat-parser'; import { createFailError } from '@osd/dev-utils'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const ESCAPE_LINE_BREAK_REGEX = /(?<!\\)\\\n/g; const HTML_LINE_BREAK_REGEX = /[\s]*\n[\s]*/g; @@ -62,7 +63,7 @@ export const accessAsync = promisify(fs.access); export const globAsync = promisify(glob); export function normalizePath(inputPath) { - return normalize(path.relative('.', inputPath)); + return normalize(path.relative(PROCESS_WORKING_DIR, inputPath)); } export function difference(left = [], right = []) { diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index ea36e33fb30b..6eb7b4013354 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -75,7 +75,12 @@ export default { coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'], coverageReporters: ['lcov', 'text-summary'], moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], - modulePathIgnorePatterns: ['__fixtures__/', 'target/', '<rootDir>/src/plugins/maps_legacy'], + modulePathIgnorePatterns: [ + '__fixtures__/', + 'target/', + '<rootDir>/src/plugins/maps_legacy', + '<rootDir>/src/cli_plugin/list/.test.data.list', + ], testEnvironment: 'jest-environment-jsdom', testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testPathIgnorePatterns: [ diff --git a/src/dev/jest/integration_tests/junit_reporter.test.js b/src/dev/jest/integration_tests/junit_reporter.test.js index e3f46c40eb16..9ae4e660d21b 100644 --- a/src/dev/jest/integration_tests/junit_reporter.test.js +++ b/src/dev/jest/integration_tests/junit_reporter.test.js @@ -36,6 +36,7 @@ import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; import { getUniqueJunitReportPath } from '@osd/test'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const MINUTE = 1000 * 60; const ROOT_DIR = resolve(__dirname, '../../../../'); @@ -44,7 +45,7 @@ const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); const XML_PATH = getUniqueJunitReportPath(FIXTURE_DIR, 'Jest Tests'); afterAll(async () => { - await del(TARGET_DIR); + await del(TARGET_DIR, { cwd: PROCESS_WORKING_DIR }); }); const parseXml = promisify(xml2js.parseString);