diff --git a/examples/data-objects/table-document/src/test/.mocharc.js b/examples/data-objects/table-document/src/test/.mocharc.js index 7bb530ceecdb..a92b7ba302bb 100644 --- a/examples/data-objects/table-document/src/test/.mocharc.js +++ b/examples/data-objects/table-document/src/test/.mocharc.js @@ -7,6 +7,6 @@ const packageDir = `${__dirname}/../..`; -const getFluidTestMochaConfig = require("@fluid-internal/test-version-utils/mocharc-common.js"); +const getFluidTestMochaConfig = require("@fluid-internal/test-version-utils/mocharc-common.cjs"); const config = getFluidTestMochaConfig(packageDir); module.exports = config; diff --git a/examples/data-objects/webflow/src/test/.mocharc.cjs b/examples/data-objects/webflow/src/test/.mocharc.cjs index 7bb530ceecdb..a92b7ba302bb 100644 --- a/examples/data-objects/webflow/src/test/.mocharc.cjs +++ b/examples/data-objects/webflow/src/test/.mocharc.cjs @@ -7,6 +7,6 @@ const packageDir = `${__dirname}/../..`; -const getFluidTestMochaConfig = require("@fluid-internal/test-version-utils/mocharc-common.js"); +const getFluidTestMochaConfig = require("@fluid-internal/test-version-utils/mocharc-common.cjs"); const config = getFluidTestMochaConfig(packageDir); module.exports = config; diff --git a/packages/test/test-end-to-end-tests/src/test/.mocharc.cjs b/packages/test/test-end-to-end-tests/src/test/.mocharc.cjs index b55b1c0572e9..f2d98f497d97 100644 --- a/packages/test/test-end-to-end-tests/src/test/.mocharc.cjs +++ b/packages/test/test-end-to-end-tests/src/test/.mocharc.cjs @@ -6,6 +6,6 @@ "use strict"; const packageDir = `${__dirname}/../..`; -const getFluidTestMochaConfig = require("@fluid-internal/test-version-utils/mocharc-common.js"); +const getFluidTestMochaConfig = require("@fluid-internal/test-version-utils/mocharc-common.cjs"); const config = getFluidTestMochaConfig(packageDir, ["source-map-support/register"]); module.exports = config; diff --git a/packages/test/test-end-to-end-tests/src/test/gc/gcReferenceUpdatesInSummary.spec.ts b/packages/test/test-end-to-end-tests/src/test/gc/gcReferenceUpdatesInSummary.spec.ts index c4a3df6967ee..c0f1533f58d9 100644 --- a/packages/test/test-end-to-end-tests/src/test/gc/gcReferenceUpdatesInSummary.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/gc/gcReferenceUpdatesInSummary.spec.ts @@ -4,75 +4,73 @@ */ import { strict as assert } from "assert"; - -import { - ContainerRuntimeFactoryWithDefaultDataStore, - DataObject, - DataObjectFactory, -} from "@fluidframework/aqueduct"; import { IContainer } from "@fluidframework/container-definitions"; import { ContainerRuntime, IContainerRuntimeOptions } from "@fluidframework/container-runtime"; import { IFluidHandle } from "@fluidframework/core-interfaces"; -import { SharedMatrix } from "@fluidframework/matrix"; +import type { SharedMatrix } from "@fluidframework/matrix"; import { Marker, ReferenceType, reservedMarkerIdKey } from "@fluidframework/merge-tree"; import { requestFluidObject } from "@fluidframework/runtime-utils"; import { ISummaryTree, SummaryType } from "@fluidframework/protocol-definitions"; -import { SharedString } from "@fluidframework/sequence"; +import type { SharedString } from "@fluidframework/sequence"; import { TelemetryNullLogger } from "@fluidframework/telemetry-utils"; import { ITestObjectProvider, waitForContainerConnection } from "@fluidframework/test-utils"; import { describeFullCompat } from "@fluid-internal/test-version-utils"; import { UndoRedoStackManager } from "@fluidframework/undo-redo"; -class TestDataObject extends DataObject { - public get _root() { - return this.root; - } +/** + * Validates this scenario: When all references to a data store are deleted, the data store is marked as unreferenced + * in the next summary. When a reference to the data store is re-added, it is marked as referenced in the next summary. + * Basically, if the handle to a data store is not stored in any DDS, its summary tree will have the "unreferenced" + * property set to true. If the handle to a data store exists or it's a root data store, its summary tree does not have + * the "unreferenced" property. + */ +describeFullCompat("GC reference updates in local summary", (getTestObjectProvider, apis) => { + const { SharedMatrix, SharedString } = apis.dds; - public get _context() { - return this.context; - } + class TestDataObject extends apis.dataRuntime.DataObject { + public get _root() { + return this.root; + } + + public get _context() { + return this.context; + } - private readonly matrixKey = "matrix"; - public matrix!: SharedMatrix; - public undoRedoStackManager!: UndoRedoStackManager; + private readonly matrixKey = "matrix"; + public matrix!: SharedMatrix; + public undoRedoStackManager!: UndoRedoStackManager; - private readonly sharedStringKey = "sharedString"; - public sharedString!: SharedString; + private readonly sharedStringKey = "sharedString"; + public sharedString!: SharedString; - protected async initializingFirstTime() { - const sharedMatrix = SharedMatrix.create(this.runtime); - this.root.set(this.matrixKey, sharedMatrix.handle); + protected async initializingFirstTime() { + const sharedMatrix = SharedMatrix.create(this.runtime); + this.root.set(this.matrixKey, sharedMatrix.handle); - const sharedString = SharedString.create(this.runtime); - this.root.set(this.sharedStringKey, sharedString.handle); - } + const sharedString = SharedString.create(this.runtime); + this.root.set(this.sharedStringKey, sharedString.handle); + } - protected async hasInitialized() { - const matrixHandle = this.root.get>(this.matrixKey); - assert(matrixHandle !== undefined, "SharedMatrix not found"); - this.matrix = await matrixHandle.get(); + protected async hasInitialized() { + const matrixHandle = this.root.get>(this.matrixKey); + assert(matrixHandle !== undefined, "SharedMatrix not found"); + this.matrix = await matrixHandle.get(); - this.undoRedoStackManager = new UndoRedoStackManager(); - this.matrix.insertRows(0, 3); - this.matrix.insertCols(0, 3); - this.matrix.openUndo(this.undoRedoStackManager); + this.undoRedoStackManager = new UndoRedoStackManager(); + this.matrix.insertRows(0, 3); + this.matrix.insertCols(0, 3); + this.matrix.openUndo(this.undoRedoStackManager); - const sharedStringHandle = this.root.get>(this.sharedStringKey); - assert(sharedStringHandle !== undefined, "SharedMatrix not found"); - this.sharedString = await sharedStringHandle.get(); + const sharedStringHandle = this.root.get>( + this.sharedStringKey, + ); + assert(sharedStringHandle !== undefined, "SharedMatrix not found"); + this.sharedString = await sharedStringHandle.get(); + } } -} -/** - * Validates this scenario: When all references to a data store are deleted, the data store is marked as unreferenced - * in the next summary. When a reference to the data store is re-added, it is marked as referenced in the next summary. - * Basically, if the handle to a data store is not stored in any DDS, its summary tree will have the "unreferenced" - * property set to true. If the handle to a data store exists or it's a root data store, its summary tree does not have - * the "unreferenced" property. - */ -describeFullCompat("GC reference updates in local summary", (getTestObjectProvider) => { let provider: ITestObjectProvider; - const factory = new DataObjectFactory( + const factory = new apis.dataRuntime.DataObjectFactory( "TestDataObject", TestDataObject, [SharedMatrix.getFactory(), SharedString.getFactory()], @@ -87,7 +85,7 @@ describeFullCompat("GC reference updates in local summary", (getTestObjectProvid }, gcOptions: { gcAllowed: true }, }; - const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore( + const runtimeFactory = new apis.containerRuntime.ContainerRuntimeFactoryWithDefaultDataStore( factory, [[factory.type, Promise.resolve(factory)]], undefined, diff --git a/packages/test/test-version-utils/.eslintrc.js b/packages/test/test-version-utils/.eslintrc.cjs similarity index 52% rename from packages/test/test-version-utils/.eslintrc.js rename to packages/test/test-version-utils/.eslintrc.cjs index e520388420dc..06a2d8c663d3 100644 --- a/packages/test/test-version-utils/.eslintrc.js +++ b/packages/test/test-version-utils/.eslintrc.cjs @@ -8,6 +8,12 @@ module.exports = { rules: { "@typescript-eslint/strict-boolean-expressions": "off", // requires strictNullChecks=true in tsconfig "import/no-nodejs-modules": "off", + // ESLint's resolver doesn't resolve relative imports of ESNext modules correctly, since + // it resolves the path relative to the .ts file (and assumes a file with a .js extension + // should exist there) + // AB#4614 tracks moving to eslint-import-resolver-typescript (which handles such imports + // out of the box) and removing this exception. + "import/no-unresolved": ["error", { ignore: ["^\\.(.*)\\.js$"] }], }, parserOptions: { project: ["./tsconfig.json", "./src/test/tsconfig.json"], diff --git a/packages/test/test-version-utils/mocharc-common.js b/packages/test/test-version-utils/mocharc-common.cjs similarity index 84% rename from packages/test/test-version-utils/mocharc-common.js rename to packages/test/test-version-utils/mocharc-common.cjs index e84818d20e62..dd7388df850d 100644 --- a/packages/test/test-version-utils/mocharc-common.js +++ b/packages/test/test-version-utils/mocharc-common.cjs @@ -4,8 +4,7 @@ */ "use strict"; - -const options = require("./dist/compatOptions"); +const options = import("./dist/compatOptions.js"); const getFluidTestMochaConfig = require("@fluidframework/mocha-test-setup/mocharc-common.js"); function getFluidTestVariant() { @@ -32,11 +31,7 @@ function getFluidTestMochaConfigWithCompat(packageDir, additionalRequiredModules testReportPrefix += `_${options.compatKind.join("_")}`; } - return getFluidTestMochaConfig( - packageDir, - ["@fluid-internal/test-version-utils", ...additionalRequiredModules], - testReportPrefix, - ); + return getFluidTestMochaConfig(packageDir, additionalRequiredModules, testReportPrefix); } module.exports = getFluidTestMochaConfigWithCompat; diff --git a/packages/test/test-version-utils/package.json b/packages/test/test-version-utils/package.json index 4199f208f821..4d33659789fd 100644 --- a/packages/test/test-version-utils/package.json +++ b/packages/test/test-version-utils/package.json @@ -11,6 +11,7 @@ "license": "MIT", "author": "Microsoft and contributors", "sideEffects": false, + "type": "module", "main": "dist/index.js", "module": "lib/index.js", "types": "dist/index.d.ts", diff --git a/packages/test/test-version-utils/src/compatConfig.ts b/packages/test/test-version-utils/src/compatConfig.ts index dae247b31e9f..18860f06174a 100644 --- a/packages/test/test-version-utils/src/compatConfig.ts +++ b/packages/test/test-version-utils/src/compatConfig.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. */ import { Lazy, assert } from "@fluidframework/common-utils"; -import { ensurePackageInstalled } from "./testApi"; -import { pkgVersion } from "./packageVersion"; +import { ensurePackageInstalled } from "./testApi.js"; +import { pkgVersion } from "./packageVersion.js"; import { CompatKind, compatKind, @@ -14,7 +14,7 @@ import { tenantIndex, baseVersion, reinstall, -} from "./compatOptions"; +} from "./compatOptions.js"; /* * Generate configuration combinations for a particular compat version @@ -229,8 +229,40 @@ export const configList = new Lazy(() => { return _configList; }); -/* +/** * Mocha start up to ensure legacy versions are installed + * @privateRemarks + * This isn't currently used in a global setup hook due to https://github.com/mochajs/mocha/issues/4508. + * Instead, we ensure that all requested compatibility versions are loaded at `describeCompat` module import time by + * leveraging top-level await. + * + * This makes compatibility layer APIs (e.g. DDSes, data object, etc.) available at mocha suite creation time rather than + * hook/test execution time, which is convenient for test authors: this sort of code can be used + * ```ts + * describeCompat("my suite", (getTestObjectProvider, apis) => { + * class MyDataObject extends apis.dataRuntime.DataObject { + * // ... + * } + * }); + * ``` + * + * instead of code like this: + * + * ```ts + * describeCompat("my suite", (getTestObjectProvider, getApis) => { + * + * const makeDataObjectClass = (apis: CompatApis) => class MyDataObject extends apis.dataRuntime.DataObject { + * // ... + * } + * + * before(() => { + * // `getApis` can only be invoked from inside a hook or test + * const MyDataObject = makeDataObjectClass(getApis()) + * }); + * }); + * ``` + * + * If the linked github issue is ever fixed, this can be once again used as a global setup fixture. */ export async function mochaGlobalSetup() { const versions = new Set(configList.value.map((value) => value.compatVersion)); diff --git a/packages/test/test-version-utils/src/compatOptions.ts b/packages/test/test-version-utils/src/compatOptions.ts index ad9acf1a829c..011914f527f2 100644 --- a/packages/test/test-version-utils/src/compatOptions.ts +++ b/packages/test/test-version-utils/src/compatOptions.ts @@ -5,8 +5,8 @@ import nconf from "nconf"; import { RouterliciousEndpoint, TestDriverTypes } from "@fluidframework/test-driver-definitions"; -import { resolveVersion } from "./versionUtils"; -import { pkgVersion } from "./packageVersion"; +import { resolveVersion } from "./versionUtils.js"; +import { pkgVersion } from "./packageVersion.js"; /** * Different kind of compat version config diff --git a/packages/test/test-version-utils/src/compatUtils.ts b/packages/test/test-version-utils/src/compatUtils.ts index 97d0f464055a..2550c4b68480 100644 --- a/packages/test/test-version-utils/src/compatUtils.ts +++ b/packages/test/test-version-utils/src/compatUtils.ts @@ -22,8 +22,14 @@ import { } from "@fluidframework/test-utils"; import { TestDriverTypes } from "@fluidframework/test-driver-definitions"; import { mixinAttributor } from "@fluid-experimental/attributor"; -import { pkgVersion } from "./packageVersion"; -import { getLoaderApi, getContainerRuntimeApi, getDataRuntimeApi, getDriverApi } from "./testApi"; +import { pkgVersion } from "./packageVersion.js"; +import { + getLoaderApi, + getContainerRuntimeApi, + getDataRuntimeApi, + getDriverApi, + CompatApis, +} from "./testApi.js"; export const TestDataObjectType = "@fluid-example/test-dataStore"; @@ -90,43 +96,28 @@ function createGetDataStoreFactoryFunction(api: ReturnType, driverConfig?: { type?: TestDriverTypes; config?: FluidTestDriverConfig; - version?: number | string; }, ) { - const driverApi = getDriverApi(baseVersion, driverConfig?.version); - return createFluidTestDriver(driverConfig?.type ?? "local", driverConfig?.config, driverApi); -} - -export async function getVersionedTestObjectProvider( - baseVersion: string, - loaderVersion?: number | string, - driverConfig?: { - type?: TestDriverTypes; - config?: FluidTestDriverConfig; - version?: number | string; - }, - runtimeVersion?: number | string, - dataRuntimeVersion?: number | string, -): Promise { - const loaderApi = getLoaderApi(baseVersion, loaderVersion); - const containerRuntimeApi = getContainerRuntimeApi(baseVersion, runtimeVersion); - const dataRuntimeApi = getDataRuntimeApi(baseVersion, dataRuntimeVersion); - const driver = await createVersionedFluidTestDriver(baseVersion, driverConfig); + const driver = await createFluidTestDriver( + driverConfig?.type ?? "local", + driverConfig?.config, + apis.driver, + ); const innerRequestHandler = async (request: IRequest, runtime: IContainerRuntimeBase) => runtime.IFluidHandleContext.resolveHandle(request); - const getDataStoreFactoryFn = createGetDataStoreFactoryFunction(dataRuntimeApi); + const getDataStoreFactoryFn = createGetDataStoreFactoryFunction(apis.dataRuntime); const containerFactoryFn = (containerOptions?: ITestContainerConfig) => { const dataStoreFactory = getDataStoreFactoryFn(containerOptions); const runtimeCtor = containerOptions?.enableAttribution === true - ? mixinAttributor(containerRuntimeApi.ContainerRuntime) - : containerRuntimeApi.ContainerRuntime; + ? mixinAttributor(apis.containerRuntime.ContainerRuntime) + : apis.containerRuntime.ContainerRuntime; const factoryCtor = createTestContainerRuntimeFactory(runtimeCtor); return new factoryCtor( TestDataObjectType, @@ -136,5 +127,27 @@ export async function getVersionedTestObjectProvider( ); }; - return new TestObjectProvider(loaderApi.Loader, driver, containerFactoryFn); + return new TestObjectProvider(apis.loader.Loader, driver, containerFactoryFn); +} + +export async function getVersionedTestObjectProvider( + baseVersion: string, + loaderVersion?: number | string, + driverConfig?: { + type?: TestDriverTypes; + config?: FluidTestDriverConfig; + version?: number | string; + }, + runtimeVersion?: number | string, + dataRuntimeVersion?: number | string, +): Promise { + return getVersionedTestObjectProviderFromApis( + { + loader: getLoaderApi(baseVersion, loaderVersion), + containerRuntime: getContainerRuntimeApi(baseVersion, runtimeVersion), + dataRuntime: getDataRuntimeApi(baseVersion, dataRuntimeVersion), + driver: getDriverApi(baseVersion, driverConfig?.version), + }, + driverConfig, + ); } diff --git a/packages/test/test-version-utils/src/describeCompat.ts b/packages/test/test-version-utils/src/describeCompat.ts index b1a2764ec1d1..343e29b8d5aa 100644 --- a/packages/test/test-version-utils/src/describeCompat.ts +++ b/packages/test/test-version-utils/src/describeCompat.ts @@ -9,15 +9,25 @@ import { ITestObjectProvider, TestObjectProvider, } from "@fluidframework/test-utils"; -import { configList } from "./compatConfig"; -import { CompatKind, baseVersion, driver, r11sEndpointName, tenantIndex } from "./compatOptions"; -import { getVersionedTestObjectProvider } from "./compatUtils"; +import { configList, mochaGlobalSetup } from "./compatConfig.js"; +import { CompatKind, baseVersion, driver, r11sEndpointName, tenantIndex } from "./compatOptions.js"; +import { getVersionedTestObjectProviderFromApis } from "./compatUtils.js"; +import { + getContainerRuntimeApi, + getDataRuntimeApi, + getLoaderApi, + CompatApis, + getDriverApi, +} from "./testApi.js"; + +// See doc comment on mochaGlobalSetup. +await mochaGlobalSetup(); /* * Mocha Utils for test to generate the compat variants. */ function createCompatSuite( - tests: (this: Mocha.Suite, provider: () => ITestObjectProvider) => void, + tests: (this: Mocha.Suite, provider: () => ITestObjectProvider, apis: CompatApis) => void, compatFilter?: CompatKind[], ) { return function (this: Mocha.Suite) { @@ -30,22 +40,24 @@ function createCompatSuite( describe(config.name, function () { let provider: TestObjectProvider; let resetAfterEach: boolean; + const dataRuntimeApi = getDataRuntimeApi(baseVersion, config.dataRuntime); + const apis: CompatApis = { + containerRuntime: getContainerRuntimeApi(baseVersion, config.containerRuntime), + dataRuntime: dataRuntimeApi, + dds: dataRuntimeApi.dds, + driver: getDriverApi(baseVersion, config.driver), + loader: getLoaderApi(baseVersion, config.loader), + }; + before(async function () { try { - provider = await getVersionedTestObjectProvider( - baseVersion, - config.loader, - { - type: driver, - version: config.driver, - config: { - r11s: { r11sEndpointName }, - odsp: { tenantIndex }, - }, + provider = await getVersionedTestObjectProviderFromApis(apis, { + type: driver, + config: { + r11s: { r11sEndpointName }, + odsp: { tenantIndex }, }, - config.containerRuntime, - config.dataRuntime, - ); + }); } catch (error) { const logger = ChildLogger.create(getTestLogger?.(), "DescribeCompatSetup"); logger.sendErrorEvent( @@ -60,13 +72,14 @@ function createCompatSuite( Object.defineProperty(this, "__fluidTestProvider", { get: () => provider }); }); + tests.bind(this)((options?: ITestObjectProviderOptions) => { resetAfterEach = options?.resetAfterEach ?? true; if (options?.syncSummarizer === true) { provider.resetLoaderContainerTracker(true /* syncSummarizerClients */); } return provider; - }); + }, apis); afterEach(function (done: Mocha.Done) { const logErrors = getUnexpectedLogErrorException(provider.logger); @@ -99,6 +112,7 @@ export type DescribeCompatSuite = ( tests: ( this: Mocha.Suite, provider: (options?: ITestObjectProviderOptions) => ITestObjectProvider, + apis: CompatApis, ) => void, ) => Mocha.Suite | void; diff --git a/packages/test/test-version-utils/src/describeE2eDocs.ts b/packages/test/test-version-utils/src/describeE2eDocs.ts index 95fc0f6d8e65..3177f6ff2c9c 100644 --- a/packages/test/test-version-utils/src/describeE2eDocs.ts +++ b/packages/test/test-version-utils/src/describeE2eDocs.ts @@ -11,10 +11,10 @@ import { ITestObjectProvider, TestObjectProvider, } from "@fluidframework/test-utils"; -import { configList } from "./compatConfig"; -import { CompatKind, baseVersion, driver, r11sEndpointName, tenantIndex } from "./compatOptions"; -import { getVersionedTestObjectProvider } from "./compatUtils"; -import { ITestObjectProviderOptions } from "./describeCompat"; +import { configList } from "./compatConfig.js"; +import { CompatKind, baseVersion, driver, r11sEndpointName, tenantIndex } from "./compatOptions.js"; +import { getVersionedTestObjectProvider } from "./compatUtils.js"; +import { ITestObjectProviderOptions } from "./describeCompat.js"; /* * Types of documents to be used during the performance runs. diff --git a/packages/test/test-version-utils/src/describeWithVersions.ts b/packages/test/test-version-utils/src/describeWithVersions.ts index 2146738da453..d7f463e91f2b 100644 --- a/packages/test/test-version-utils/src/describeWithVersions.ts +++ b/packages/test/test-version-utils/src/describeWithVersions.ts @@ -8,11 +8,11 @@ import { ITestObjectProvider, TestObjectProvider, } from "@fluidframework/test-utils"; -import { driver, r11sEndpointName, tenantIndex } from "./compatOptions"; -import { getVersionedTestObjectProvider } from "./compatUtils"; -import { ITestObjectProviderOptions } from "./describeCompat"; -import { pkgVersion } from "./packageVersion"; -import { ensurePackageInstalled, InstalledPackage } from "./testApi"; +import { driver, r11sEndpointName, tenantIndex } from "./compatOptions.js"; +import { getVersionedTestObjectProvider } from "./compatUtils.js"; +import { ITestObjectProviderOptions } from "./describeCompat.js"; +import { pkgVersion } from "./packageVersion.js"; +import { ensurePackageInstalled, InstalledPackage } from "./testApi.js"; /** * Interface to hold the requested versions which should be installed diff --git a/packages/test/test-version-utils/src/index.ts b/packages/test/test-version-utils/src/index.ts index 98ed67f4815b..7535fba3426e 100644 --- a/packages/test/test-version-utils/src/index.ts +++ b/packages/test/test-version-utils/src/index.ts @@ -2,14 +2,15 @@ * Copyright (c) Microsoft Corporation and contributors. All rights reserved. * Licensed under the MIT License. */ -export { mochaGlobalSetup } from "./compatConfig"; +export { mochaGlobalSetup } from "./compatConfig.js"; export { getDataStoreFactory, getVersionedTestObjectProvider, + getVersionedTestObjectProviderFromApis, ITestDataObject, TestDataObjectType, -} from "./compatUtils"; -export { describeInstallVersions } from "./describeWithVersions"; +} from "./compatUtils.js"; +export { describeInstallVersions } from "./describeWithVersions.js"; export { DescribeCompat, DescribeCompatSuite, @@ -17,7 +18,7 @@ export { describeLoaderCompat, describeNoCompat, ITestObjectProviderOptions, -} from "./describeCompat"; +} from "./describeCompat.js"; export { describeE2EDocs, DocumentType, @@ -34,12 +35,13 @@ export { assertDocumentTypeInfo, isDocumentMapInfo, isDocumentMultipleDataStoresInfo, -} from "./describeE2eDocs"; -export { ExpectedEvents, ExpectsTest, itExpects } from "./itExpects"; +} from "./describeE2eDocs.js"; +export { ExpectedEvents, ExpectsTest, itExpects } from "./itExpects.js"; export { + CompatApis, ensurePackageInstalled, getContainerRuntimeApi, getDataRuntimeApi, getDriverApi, getLoaderApi, -} from "./testApi"; +} from "./testApi.js"; diff --git a/packages/test/test-version-utils/src/test/tsconfig.json b/packages/test/test-version-utils/src/test/tsconfig.json index f48772395330..312c7f582e9a 100644 --- a/packages/test/test-version-utils/src/test/tsconfig.json +++ b/packages/test/test-version-utils/src/test/tsconfig.json @@ -7,6 +7,7 @@ "declaration": false, "declarationMap": false, "skipLibCheck": true, + "module": "esnext", }, "include": ["./**/*"], "references": [ diff --git a/packages/test/test-version-utils/src/test/versionUtils.spec.ts b/packages/test/test-version-utils/src/test/versionUtils.spec.ts index a052e096a6f2..3e7393ac40a5 100644 --- a/packages/test/test-version-utils/src/test/versionUtils.spec.ts +++ b/packages/test/test-version-utils/src/test/versionUtils.spec.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ import { strict as assert } from "assert"; -import { getRequestedRange, versionHasMovedSparsedMatrix } from "../versionUtils"; +import { getRequestedRange, versionHasMovedSparsedMatrix } from "../versionUtils.js"; describe("versionUtils", () => { it("Get the major version number above or below the baseVersion", () => { diff --git a/packages/test/test-version-utils/src/testApi.ts b/packages/test/test-version-utils/src/testApi.ts index a612481083cb..59e04a9c5db2 100644 --- a/packages/test/test-version-utils/src/testApi.ts +++ b/packages/test/test-version-utils/src/testApi.ts @@ -32,14 +32,14 @@ import { import { SparseMatrix } from "@fluid-experimental/sequence-deprecated"; import * as semver from "semver"; -import { pkgVersion } from "./packageVersion"; +import { pkgVersion } from "./packageVersion.js"; import { checkInstalled, ensureInstalled, getRequestedRange, loadPackage, versionHasMovedSparsedMatrix, -} from "./versionUtils"; +} from "./versionUtils.js"; // List of package that needs to be install for legacy versions const packageList = [ @@ -69,8 +69,23 @@ export const ensurePackageInstalled = async ( baseVersion: string, version: number | string, force: boolean, -): Promise => - ensureInstalled(getRequestedRange(baseVersion, version), packageList, force); +): Promise => { + const pkg = await ensureInstalled(getRequestedRange(baseVersion, version), packageList, force); + await Promise.all([ + loadContainerRuntime(baseVersion, version), + loadDataRuntime(baseVersion, version), + loadLoader(baseVersion, version), + loadDriver(baseVersion, version), + ]); + return pkg; +}; + +// This module supports synchronous functions to import packages once their install has been completed. +// Since dynamic import is async, we thus cache the modules based on their package version. +const loaderCache = new Map(); +const containerRuntimeCache = new Map(); +const dataRuntimeCache = new Map(); +const driverCache = new Map(); // Current versions of the APIs const LoaderApi = { @@ -103,6 +118,166 @@ const DataRuntimeApi = { }, }; +async function loadLoader(baseVersion: string, requested?: number | string): Promise { + const requestedStr = getRequestedRange(baseVersion, requested); + if (semver.satisfies(pkgVersion, requestedStr)) { + return; + } + + const { version, modulePath } = checkInstalled(requestedStr); + if (!loaderCache.has(version)) { + const loader = { + version, + Loader: (await loadPackage(modulePath, "@fluidframework/container-loader")).Loader, + }; + loaderCache.set(version, loader); + } +} + +async function loadContainerRuntime( + baseVersion: string, + requested?: number | string, +): Promise { + const requestedStr = getRequestedRange(baseVersion, requested); + if (semver.satisfies(pkgVersion, requestedStr)) { + return; + } + + const { version, modulePath } = checkInstalled(requestedStr); + if (!containerRuntimeCache.has(version)) { + const containerRuntime = { + version, + ContainerRuntime: (await loadPackage(modulePath, "@fluidframework/container-runtime")) + .ContainerRuntime, + ContainerRuntimeFactoryWithDefaultDataStore: ( + await loadPackage(modulePath, "@fluidframework/aqueduct") + ).ContainerRuntimeFactoryWithDefaultDataStore, + }; + containerRuntimeCache.set(version, containerRuntime); + } +} + +async function loadDataRuntime(baseVersion: string, requested?: number | string): Promise { + const requestedStr = getRequestedRange(baseVersion, requested); + if (semver.satisfies(pkgVersion, requestedStr)) { + return; + } + const { version, modulePath } = checkInstalled(requestedStr); + if (!dataRuntimeCache.has(version)) { + /* eslint-disable @typescript-eslint/no-shadow */ + const [ + { DataObject, DataObjectFactory }, + { TestFluidObjectFactory }, + { SharedMap, SharedDirectory }, + { SharedString }, + { SharedCell }, + { SharedCounter }, + { SharedMatrix }, + { Ink }, + { ConsensusQueue }, + { ConsensusRegisterCollection }, + { SparseMatrix }, + ] = await Promise.all([ + loadPackage(modulePath, "@fluidframework/aqueduct"), + loadPackage(modulePath, "@fluidframework/test-utils"), + loadPackage(modulePath, "@fluidframework/map"), + loadPackage(modulePath, "@fluidframework/sequence"), + loadPackage(modulePath, "@fluidframework/cell"), + loadPackage(modulePath, "@fluidframework/counter"), + loadPackage(modulePath, "@fluidframework/matrix"), + loadPackage(modulePath, "@fluidframework/ink"), + loadPackage(modulePath, "@fluidframework/ordered-collection"), + loadPackage(modulePath, "@fluidframework/register-collection"), + loadPackage( + modulePath, + versionHasMovedSparsedMatrix(version) + ? "@fluid-experimental/sequence-deprecated" + : "@fluidframework/sequence", + ), + ]); + /* eslint-enable @typescript-eslint/no-shadow */ + + const dataRuntime = { + version, + DataObject, + DataObjectFactory, + TestFluidObjectFactory, + dds: { + SharedCell, + SharedCounter, + Ink, + SharedDirectory, + SharedMap, + SharedMatrix, + ConsensusQueue, + ConsensusRegisterCollection, + SharedString, + SparseMatrix, + }, + }; + dataRuntimeCache.set(version, dataRuntime); + } +} + +async function loadDriver(baseVersion: string, requested?: number | string): Promise { + const requestedStr = getRequestedRange(baseVersion, requested); + if (semver.satisfies(pkgVersion, requestedStr)) { + return; + } + + const { version, modulePath } = checkInstalled(requestedStr); + if (!driverCache.has(version)) { + const [ + { LocalDocumentServiceFactory, LocalResolver, createLocalResolverCreateNewRequest }, + { LocalDeltaConnectionServer }, + { + OdspDocumentServiceFactory, + OdspDriverUrlResolver, + createOdspCreateContainerRequest, + createOdspUrl, + }, + { RouterliciousDocumentServiceFactory }, + ] = await Promise.all([ + loadPackage(modulePath, "@fluidframework/local-driver"), + loadPackage(modulePath, "@fluidframework/server-local-server"), + loadPackage(modulePath, "@fluidframework/odsp-driver"), + loadPackage(modulePath, "@fluidframework/routerlicious-driver"), + ]); + + const LocalDriverApi: typeof DriverApi.LocalDriverApi = { + version, + LocalDocumentServiceFactory, + LocalResolver, + LocalDeltaConnectionServer, + createLocalResolverCreateNewRequest, + }; + + const OdspDriverApi: typeof DriverApi.OdspDriverApi = { + version, + OdspDocumentServiceFactory, + OdspDriverUrlResolver, + createOdspCreateContainerRequest, + createOdspUrl, + }; + + const RouterliciousDriverApi: typeof DriverApi.RouterliciousDriverApi = { + version, + modulePath, + RouterliciousDocumentServiceFactory, + }; + + driverCache.set(version, { + LocalDriverApi, + OdspDriverApi, + RouterliciousDriverApi, + }); + } +} + +function throwNotFound(layer: string, version: string): never { + throw new Error(`${layer}@${version} not found. Missing install step?`); +} + export function getLoaderApi(baseVersion: string, requested?: number | string): typeof LoaderApi { const requestedStr = getRequestedRange(baseVersion, requested); @@ -111,11 +286,9 @@ export function getLoaderApi(baseVersion: string, requested?: number | string): return LoaderApi; } - const { version, modulePath } = checkInstalled(requestedStr); - return { - version, - Loader: loadPackage(modulePath, "@fluidframework/container-loader").Loader, - }; + const { version } = checkInstalled(requestedStr); + const loaderApi = loaderCache.get(version); + return loaderApi ?? throwNotFound("Loader", version); } export function getContainerRuntimeApi( @@ -126,16 +299,8 @@ export function getContainerRuntimeApi( if (semver.satisfies(pkgVersion, requestedStr)) { return ContainerRuntimeApi; } - const { version, modulePath } = checkInstalled(requestedStr); - return { - version, - ContainerRuntime: loadPackage(modulePath, "@fluidframework/container-runtime") - .ContainerRuntime, - ContainerRuntimeFactoryWithDefaultDataStore: loadPackage( - modulePath, - "@fluidframework/aqueduct", - ).ContainerRuntimeFactoryWithDefaultDataStore, - }; + const { version } = checkInstalled(requestedStr); + return containerRuntimeCache.get(version) ?? throwNotFound("ContainerRuntime", version); } export function getDataRuntimeApi( @@ -146,35 +311,8 @@ export function getDataRuntimeApi( if (semver.satisfies(pkgVersion, requestedStr)) { return DataRuntimeApi; } - const { version, modulePath } = checkInstalled(requestedStr); - return { - version, - DataObject: loadPackage(modulePath, "@fluidframework/aqueduct").DataObject, - DataObjectFactory: loadPackage(modulePath, "@fluidframework/aqueduct").DataObjectFactory, - TestFluidObjectFactory: loadPackage(modulePath, "@fluidframework/test-utils") - .TestFluidObjectFactory, - dds: { - SharedCell: loadPackage(modulePath, "@fluidframework/cell").SharedCell, - SharedCounter: loadPackage(modulePath, "@fluidframework/counter").SharedCounter, - Ink: loadPackage(modulePath, "@fluidframework/ink").Ink, - SharedDirectory: loadPackage(modulePath, "@fluidframework/map").SharedDirectory, - SharedMap: loadPackage(modulePath, "@fluidframework/map").SharedMap, - SharedMatrix: loadPackage(modulePath, "@fluidframework/matrix").SharedMatrix, - ConsensusQueue: loadPackage(modulePath, "@fluidframework/ordered-collection") - .ConsensusQueue, - ConsensusRegisterCollection: loadPackage( - modulePath, - "@fluidframework/register-collection", - ).ConsensusRegisterCollection, - SharedString: loadPackage(modulePath, "@fluidframework/sequence").SharedString, - SparseMatrix: loadPackage( - modulePath, - versionHasMovedSparsedMatrix(version) - ? "@fluid-experimental/sequence-deprecated" - : "@fluidframework/sequence", - ).SparseMatrix, - }, - }; + const { version } = checkInstalled(requestedStr); + return dataRuntimeCache.get(version) ?? throwNotFound("DataRuntime", version); } export function getDriverApi(baseVersion: string, requested?: number | string): typeof DriverApi { @@ -185,41 +323,14 @@ export function getDriverApi(baseVersion: string, requested?: number | string): return DriverApi; } - const { version, modulePath } = checkInstalled(requestedStr); - const localDriverApi: typeof DriverApi.LocalDriverApi = { - version, - LocalDocumentServiceFactory: loadPackage(modulePath, "@fluidframework/local-driver") - .LocalDocumentServiceFactory, - LocalResolver: loadPackage(modulePath, "@fluidframework/local-driver").LocalResolver, - LocalDeltaConnectionServer: loadPackage(modulePath, "@fluidframework/server-local-server") - .LocalDeltaConnectionServer, - createLocalResolverCreateNewRequest: loadPackage(modulePath, "@fluidframework/local-driver") - .createLocalResolverCreateNewRequest, - }; - - const odspDriverApi: typeof DriverApi.OdspDriverApi = { - version, - OdspDocumentServiceFactory: loadPackage(modulePath, "@fluidframework/odsp-driver") - .OdspDocumentServiceFactory, - OdspDriverUrlResolver: loadPackage(modulePath, "@fluidframework/odsp-driver") - .OdspDriverUrlResolver, - createOdspCreateContainerRequest: loadPackage(modulePath, "@fluidframework/odsp-driver") - .createOdspCreateContainerRequest, - createOdspUrl: loadPackage(modulePath, "@fluidframework/odsp-driver").createOdspUrl, - }; - - const routerliciousDriverApi: typeof DriverApi.RouterliciousDriverApi = { - version, - modulePath, - RouterliciousDocumentServiceFactory: loadPackage( - modulePath, - "@fluidframework/routerlicious-driver", - ).RouterliciousDocumentServiceFactory, - }; - - return { - LocalDriverApi: localDriverApi, - OdspDriverApi: odspDriverApi, - RouterliciousDriverApi: routerliciousDriverApi, - }; + const { version } = checkInstalled(requestedStr); + return driverCache.get(version) ?? throwNotFound("Driver", version); +} + +export interface CompatApis { + containerRuntime: ReturnType; + dataRuntime: ReturnType; + dds: ReturnType["dds"]; + driver: ReturnType; + loader: ReturnType; } diff --git a/packages/test/test-version-utils/src/versionUtils.ts b/packages/test/test-version-utils/src/versionUtils.ts index f20d3badd757..0f6609f37100 100644 --- a/packages/test/test-version-utils/src/versionUtils.ts +++ b/packages/test/test-version-utils/src/versionUtils.ts @@ -7,15 +7,16 @@ import { ExecOptions, exec, execSync } from "child_process"; import * as path from "path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { existsSync, mkdirSync, rmdirSync, readdirSync, readFileSync, writeFileSync } from "fs"; import { lock } from "proper-lockfile"; import * as semver from "semver"; -import { pkgVersion } from "./packageVersion"; -import { InstalledPackage } from "./testApi"; +import { pkgVersion } from "./packageVersion.js"; +import { InstalledPackage } from "./testApi.js"; // Assuming this file is in dist\test, so go to ..\node_modules\.legacy as the install location -const baseModulePath = path.join(__dirname, "..", "node_modules", ".legacy"); +const baseModulePath = fileURLToPath(new URL("../node_modules/.legacy", import.meta.url)); const installedJsonPath = path.join(baseModulePath, "installed.json"); const getModulePath = (version: string) => path.join(baseModulePath, version); @@ -33,7 +34,7 @@ async function ensureInstalledJson() { if (existsSync(installedJsonPath)) { return; } - const release = await lock(__dirname, { retries: { forever: true } }); + const release = await lock(fileURLToPath(import.meta.url), { retries: { forever: true } }); try { // Check it again under the lock if (existsSync(installedJsonPath)) { @@ -190,7 +191,7 @@ export async function ensureInstalled( adjustedPackageList.push("@fluid-experimental/sequence-deprecated"); } - // Release the __dirname but lock the modulePath so we can do parallel installs + // Release the base path but lock the modulePath so we can do parallel installs const release = await lock(modulePath, { retries: { forever: true } }); try { if (force) { @@ -261,10 +262,8 @@ export function checkInstalled(requested: string) { ); } -export const loadPackage = (modulePath: string, pkg: string) => - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return - require(path.join(modulePath, "node_modules", pkg)); - +export const loadPackage = async (modulePath: string, pkg: string): Promise => + import(pathToFileURL(path.join(modulePath, "node_modules", pkg, "dist", "index.js")).href); /** * Used to get the major version number above or below the baseVersion. * @param baseVersion - The base version to move from (eg. "0.60.0") diff --git a/packages/test/test-version-utils/tsconfig.json b/packages/test/test-version-utils/tsconfig.json index 9f03e2b6e02c..3af7855e86b9 100644 --- a/packages/test/test-version-utils/tsconfig.json +++ b/packages/test/test-version-utils/tsconfig.json @@ -5,6 +5,7 @@ "rootDir": "./src", "outDir": "./dist", "composite": true, + "module": "esnext", }, "include": ["src/**/*"], }